見出しを8方向に分類するときにif / else ifチェーンを回避する方法は?


111

私は次のコードを持っています:

if (this->_car.getAbsoluteAngle() <= 30 || this->_car.getAbsoluteAngle() >= 330)
  this->_car.edir = Car::EDirection::RIGHT;
else if (this->_car.getAbsoluteAngle() > 30 && this->_car.getAbsoluteAngle() <= 60)
  this->_car.edir = Car::EDirection::UP_RIGHT;
else if (this->_car.getAbsoluteAngle() > 60 && this->_car.getAbsoluteAngle() <= 120)
  this->_car.edir = Car::EDirection::UP;
else if (this->_car.getAbsoluteAngle() > 120 && this->_car.getAbsoluteAngle() <= 150)
  this->_car.edir = Car::EDirection::UP_LEFT;
else if (this->_car.getAbsoluteAngle() > 150 && this->_car.getAbsoluteAngle() <= 210)
  this->_car.edir = Car::EDirection::LEFT;
else if (this->_car.getAbsoluteAngle() > 210 && this->_car.getAbsoluteAngle() <= 240)
  this->_car.edir = Car::EDirection::DOWN_LEFT;
else if (this->_car.getAbsoluteAngle() > 240 && this->_car.getAbsoluteAngle() <= 300)
  this->_car.edir = Car::EDirection::DOWN;
else if (this->_car.getAbsoluteAngle() > 300 && this->_car.getAbsoluteAngle() <= 330)
  this->_car.edir = Car::EDirection::DOWN_RIGHT;

ifsチェーンを避けたいです。それは本当に醜いです。これを書く別の、おそらくよりクリーンな方法はありますか?


77
@Oraekiathis->_car.getAbsoluteAngle()カスケード全体の前に1回タッチすると、見栄えが格段に悪くなり、入力が少なくなり、読みやすくなります。
πάνταῥεῖ

26
明示的な参照解除することをすべてのthisthis->)が必要とされていないと、本当に読みやすさには何も良いしません...
ジェスパーユール

2
キーとしての@Neilペア、値としての列挙、カスタムルックアップラムダ。
πάνταῥεῖ

56
これらのすべての>テストを行わなければ、コードの見栄えははるかに悪くなります。これらはそれぞれ前のifステートメントで(反対方向に)テスト済みであるため、必要ありません。
ピートベッカー

10
@PeteBeckerそれはこのようなコードについて私のペットのおしっこの1つです。理解できないプログラマが多すぎますelse if
Barmar

回答:


176
#include <iostream>

enum Direction { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT };

Direction GetDirectionForAngle(int angle)
{
    const Direction slices[] = { RIGHT, UP_RIGHT, UP, UP, UP_LEFT, LEFT, LEFT, DOWN_LEFT, DOWN, DOWN, DOWN_RIGHT, RIGHT };
    return slices[(((angle % 360) + 360) % 360) / 30];
}

int main()
{
    // This is just a test case that covers all the possible directions
    for (int i = 15; i < 360; i += 30)
        std::cout << GetDirectionForAngle(i) << ' ';

    return 0;
}

これが私のやり方です。(私の以前のコメントに従って)。


92
文字通り、列挙型の代わりにコナミ規約を一瞬見たと思っていました。
Zano

21
@CodesInChaos:C99およびCは、C#と同じ要件を持つ++:もしことq = a/br = a%b、その後q * b + rに等しくなければなりませんa。したがって、C99では残りが負であることは合法です。BorgLeader、問題はで修正できます(((angle % 360) + 360) % 360) / 30
Eric Lippert、2017年

7
@ericlippert、あなたとあなたの計算数学の知識は感動し続けています。
gregsdennis

33
これは非常に賢いですが、完全に読み取り不可能であり、保守可能になる可能性が低くなるため、オリジナルの見た目の「醜さ」に対する適切な解決策であるとは思いません。ここには個人的な好みの要素があると思いますが、x4uとmotoDrizztによってクリーンアップされたブランチバージョンが非常に望ましいと思います。
IMSoP 2017年

4
@cyanbeamメインのforループは単なる「デモ」でGetDirectionForAngleあり、if / elseカスケードの代わりとして提案しているものです。どちらもO(1)です...
Borgleader

71

map::lower_boundマップ内の各角度の上限を使用して保存できます。

以下の作業例:

#include <cassert>
#include <map>

enum Direction
{
    RIGHT,
    UP_RIGHT,
    UP,
    UP_LEFT,
    LEFT,
    DOWN_LEFT,
    DOWN,
    DOWN_RIGHT
};

using AngleDirMap = std::map<int, Direction>;

AngleDirMap map = {
    { 30, RIGHT },
    { 60, UP_RIGHT },
    { 120, UP },
    { 150, UP_LEFT },
    { 210, LEFT },
    { 240, DOWN_LEFT },
    { 300, DOWN },
    { 330, DOWN_RIGHT },
    { 360, RIGHT }
};

Direction direction(int angle)
{
    assert(angle >= 0 && angle <= 360);

    auto it = map.lower_bound(angle);
    return it->second;
}

int main()
{
    Direction d;

    d = direction(45);
    assert(d == UP_RIGHT);

    d = direction(30);
    assert(d == RIGHT);

    d = direction(360);
    assert(d == RIGHT);

    return 0;
}

除算は必要ありません。良い!
O.ジョーンズ

17
@ O.Jones:コンパイル時定数による除算はかなり安く、乗算といくつかのシフトだけです。それはtable[angle%360/30]安くて枝がないので、私は答えの1つに行くでしょう。 これがソースに似たasmにコンパイルされる場合、ツリー検索ループよりはるかに安価です。(std::unordered_map通常はハッシュテーブルですが、通常はstd::map赤黒の二分木です。受け入れられた回答はangle%360 / 30、角度の完全なハッシュ関数として効果的に使用されます(カップルエントリを複製した後、Bijayの回答はそれをオフセットで回避します))。
Peter Cordes

2
lower_boundソートされた配列で使用できます。これは、よりもはるかに効率的ですmap
wilx 2017年

@PeterCordesのマップルックアップは、記述と保守が簡単です。範囲が変更された場合、ハッシュコードを更新するとバグが発生する可能性があり、範囲が不均一になると、単にばらばらになる可能性があります。そのコードがパフォーマンスクリティカルでない限り、私は気にしないでしょう。
OhJeez 2017年

@OhJeez:それらはすでに不均一であり、複数のバケットで同じ値を持つことによって処理されます。非常に小さな除数を使用してバケットが多すぎることを意味しない限り、より小さな除数を使用してより多くのバケットを取得します。また、パフォーマンスが重要ではない場合this->_car.getAbsoluteAngle()、tmp varを使用して因数分解し、各OP if()句から冗長な比較を削除することで単純化されている場合、if / elseチェーンも悪くありません(すでに一致しているものをチェックします)前述のif())。または、@ wilxのソートされた配列の提案を使用します。
Peter Cordes

58

各要素が30度のブロックに関連付けられている配列を作成します。

Car::EDirection dirlist[] = { 
    Car::EDirection::RIGHT, 
    Car::EDirection::UP_RIGHT, 
    Car::EDirection::UP, 
    Car::EDirection::UP, 
    Car::EDirection::UP_LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::DOWN_LEFT,
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN_RIGHT, 
    Car::EDirection::RIGHT
};

次に、角度/ 30で配列にインデックスを付けることができます。

this->_car.edir = dirlist[(this->_car.getAbsoluteAngle() % 360) / 30];

比較や分岐は必要ありません。

ただし、結果はオリジナルとは少し異なります。30、60、120などの境界線上の値は、次のカテゴリに配置されます。たとえば、元のコードの有効な値はUP_RIGHT31〜60です。上記のコードでは、30〜59をに割り当てていUP_RIGHTます。

これを回避するには、角度から1を減算します。

this->_car.edir = dirlist[((this->_car.getAbsoluteAngle() - 1) % 360) / 30];

これRIGHTUP_RIGHT、30、60などが得られます。

0の場合、式はになり(-1 % 360) / 30ます。これは-1 % 360 == -1および-1 / 30 == 0であるため有効であり、インデックス0を取得します。

C ++標準のセクション5.6は、この動作を確認しています。

4二項/演算子は商を生成し、二項%演算子は最初の式を2番目の式で除算した余りを生成します。/またはの2番目のオペランド%がゼロの場合、動作は未定義です。整数オペランドの場合、/演算子は、小数部分が破棄された代数商を生成します。商a/bが結果の型で表現できる場合、 (a/b)*b + a%bはに等しいa

編集:

このような構造の可読性と保守性に関しては、多くの疑問が提起されました。motoDrizztによって与えられた答えは、より保守しやすく、「醜い」ほどではない元の構成を単純化する良い例です。

彼の答えを拡張して、三項演算子を使用する別の例を次に示します。元の投稿の各ケースが同じ変数に割り当てられているため、この演算子を使用すると読みやすさがさらに向上します。

int angle = ((this->_car.getAbsoluteAngle() % 360) + 360) % 360;

this->_car.edir = (angle <= 30)  ?  Car::EDirection::RIGHT :
                  (angle <= 60)  ?  Car::EDirection::UP_RIGHT :
                  (angle <= 120) ?  Car::EDirection::UP :
                  (angle <= 150) ?  Car::EDirection::UP_LEFT :
                  (angle <= 210) ?  Car::EDirection::LEFT : 
                  (angle <= 240) ?  Car::EDirection::DOWN_LEFT :
                  (angle <= 300) ?  Car::EDirection::DOWN:  
                  (angle <= 330) ?  Car::EDirection::DOWN_RIGHT :
                                    Car::EDirection::RIGHT;

49

そのコードは見苦しくありません。シンプルで実用的、読みやすく、理解しやすいコードです。それは独自の方法で分離されるので、日常生活でそれを扱う必要はありません。そして誰かがそれをチェックしなければならない場合に備えて-多分彼はどこか他の場所で問題についてあなたのアプリケーションをデバッグしているためかもしれません-それはとても簡単で、コードとそれが何をするか理解するのに2秒かかります。

私がそのようなデバッグを行っていたなら、関数が何をしているかを理解するために5分間費やす必要がないことを嬉しく思います。これに関して、他のすべての関数は完全に失敗します。それらは、デバッグの際に深く分析してテストすることを強いられる複雑な混乱の中で、単純で忘れがちなバグのないルーチンを変更するためです。私自身、プロジェクトマネージャーとして、開発者が単純なタスクを実行し、単純で無害な方法で実装するのではなく、過度に複雑な方法で実装するのに時間を浪費することに強く腹を立てます。あなたがそれについて考えて無駄な時間をすべて考えてから、SOの質問に来るようにしてください。

そうは言っても、コードを読みにくくする一般的な間違いがあり、簡単にできるいくつかの改善点があります。

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;
else if (angle <= 60)
  return Car::EDirection::UP_RIGHT;
else if (angle <= 120)
  return Car::EDirection::UP;
else if (angle <= 150)
  return Car::EDirection::UP_LEFT;
else if (angle <= 210)
  return Car::EDirection::LEFT;
else if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;
else if (angle <= 300)
  return Car::EDirection::DOWN;
else if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

これをメソッドに入れ、戻り値をオブジェクトに割り当て、メソッドを折りたたみ、それを永遠に忘れます。

PS 330のしきい値を超える別のバグがありますが、どのように処理したいのかわからないため、まったく修正しませんでした。


後で更新

コメントに従って、もしあれば、elseを取り除くこともできます:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;

if (angle <= 60)
  return Car::EDirection::UP_RIGHT;

if (angle <= 120)
  return Car::EDirection::UP;

if (angle <= 150)
  return Car::EDirection::UP_LEFT;

if (angle <= 210)
  return Car::EDirection::LEFT;

if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;

if (angle <= 300)
  return Car::EDirection::DOWN;

if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

特定のポイントは自分の好みの問題になると思うので、私はそれをしませんでした。私の答えの範囲は、「コードの醜さ」に関するあなたの懸念に別の視点を与えることでした(現在もそうです)。とにかく、私が言ったように、誰かがコメントでそれを指摘しました、そしてそれを示すことは理にかなっていると思います。


1
一体何を成し遂げるのだろう?
抽象化がすべてです。

8
このルートを使いたい場合は、少なくとも不要なものを取り除く必要else ififあります。これで十分です。
2017年

10
@Ð私は完全に同意しませんelse if。コードブロックを一目で確認できると便利です。関連のないステートメントのグループではなく、ディシジョンツリーであることがわかります。はい、elseまたはの後のコンパイラbreakは必要ありませんが、コードをちらっと見ている人間にとっては便利です。return
IMSoP 2017年

@Ð追加のネストが必要な言語を見たことがありません。別のelseif/ elsifキーワードがあるか、技術的にはif、ここと同じように、たまたま始まる1つのステートメントブロックを使用しています。私があなたが考えていることと私が考えていることの簡単な例:gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP

7
@Ðはい、私はそれが恐ろしいことであることに同意します。しかしelse、それはあなたがそうすることを作っているのではなく、それelse ifが明確な声明として認識されない貧弱なスタイルガイドです。私はいつも中かっこを使用しましたが、私の要点で示したように、そのようなコードを書くことは決してありません。
IMSoP 2017年

39

疑似コード:

angle = (angle + 30) %360; // Offset by 30. 

そこで、我々は持っている0-6060-9090-150、...カテゴリとして。90度の各象限では、1つの部分は60で、1つの部分は30です。したがって、次のようになります。

i = angle / 90; // Figure out the quadrant. Could be 0, 1, 2, 3 

j = (angle - 90 * i) >= 60? 1: 0; // In the quardrant is it perfect (eg: RIGHT) or imperfect (eg: UP_RIGHT)?

index = i * 2 + j;

enumを適切な順序で含む配列でインデックスを使用します。


7
これは良い、おそらくここでの最良の答えです。おそらく、元の質問者が後で列挙型の使用を調べた場合、それが数値に変換されているだけのケースがあることに気付くでしょう。enumを完全に削除し、方向整数をそのまま使用することは、おそらく彼のコードの他の場所でも意味があり、この答えはあなたをそこに直接導きます。
ビルK

18
switch (this->_car.getAbsoluteAngle() / 30) // integer division
{
    case 0:
    case 11: this->_car.edir = Car::EDirection::RIGHT; break;
    case 1: this->_car.edir = Car::EDirection::UP_RIGHT; break;
    ...
    case 10: this->_car.edir = Car::EDirection::DOWN_RIGHT; break;
}

角度= this-> _ car.getAbsoluteAngle(); セクター=(角度%360)/ 30; 結果は12セクターです。次に、配列にインデックスを付けるか、上記のようにスイッチ/ケースを使用します-コンパイラはとにかくジャンプテーブルに変換します。
ChuckCottrill

1
if / elseチェーンよりもスイッチの方が優れています。
ビルK

5
@BillK:コンパイラがそれをテーブルルックアップに変換する場合は、そうなる可能性があります。if / elseチェーンよりも可能性が高いです。しかし、それは簡単で、アーキテクチャ固有のトリックを必要としないため、ソースコードでテーブルルックアップを記述することをお勧めします。
Peter Cordes

通常、パフォーマンスは問題になりません-読みやすさと保守性です-すべてのスイッチとif / elseチェーンは通常、新しいアイテムを追加するたびに複数の場所で更新する必要のある厄介なコピーと貼り付けのコードの束を意味します。両方を回避し、テーブル、計算をディスパッチするか、ファイルからデータをロードして、可能であればデータとして扱うことをお勧めします。
ビルK

PeterCordesコンパイラーは、LUTとスイッチの場合と同じコードを生成する可能性があります。@BillKでは、スイッチを0..12-> Car :: EDirection関数に抽出できます。これは、LUTと同等の繰り返しになります
Caleth

16

if少し特殊なケースである最初のものを無視すると、残りのすべてはまったく同じパターンに従います。最小、最大、方向。疑似コード:

if (angle > min && angle <= max)
  _car.edir = direction;

これを実際のC ++にすると、次のようになります。

enum class EDirection {  NONE,
   RIGHT, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT, DOWN, DOWN_RIGHT };

struct AngleRange
{
    int min, max;
    EDirection direction;
};

さて、ifsの束を書くのではなく、さまざまな可能性をループしてください。

EDirection direction_from_angle(int angle, const std::vector<AngleRange>& angleRanges)
{
    for (auto&& angleRange : angleRanges)
    {
        if ((angle > angleRange.min) && (angle <= angleRange.max))
            return angleRange.direction;
    }

    return EDirection::NONE;
}

(別のオプションthrowとして、returnING ではなく例外を使用しNONEます)。

どちらを呼び出すか:

_car.edir = direction_from_angle(_car.getAbsoluteAngle(), {
    {30, 60, EDirection::UP_RIGHT},
    {60, 120, EDirection::UP},
    // ... etc.
});

この手法は、データ駆動型プログラミングと呼ばれますifsの束を取り除くことに加えて、他のコードを作り直すことなく、簡単に方向を追加したり(たとえば、NNW)、数を減らしたり(左、右、上、下)することができます。


(最初の特別なケースの処理は、「読者のための演習」として残されています。:-))


1
技術的には、すべての角度範囲が一致していればminを排除できます。これにより、のif(angle <= angleRange.max)ようなC ++ 11機能を使用する場合に条件が+1に減少しますenum class
Pharap、2017年

12

のルックアップテーブルに基づく提案されたバリアントangle / 30がおそらく望ましいですが、ハードコーディングされたバイナリ検索を使用して比較の数を最小限に抑える代替策を次に示します。

static Car::EDirection directionFromAngle( int angle )
{
    if( angle <= 210 )
    {
        if( angle > 120 )
            return angle > 150 ? Car::EDirection::LEFT
                               : Car::EDirection::UP_LEFT;
        if( angle > 30 )
            return angle > 60 ? Car::EDirection::UP
                              : Car::EDirection::UP_RIGHT;
    }
    else // > 210
    {
        if( angle <= 300 )
            return angle > 240 ? Car::EDirection::DOWN
                               : Car::EDirection::DOWN_LEFT;
        if( angle <= 330 )
            return Car::EDirection::DOWN_RIGHT;
    }
    return Car::EDirection::RIGHT; // <= 30 || > 330
}

2

重複を避けたい場合は、数式で表すことができます。

まず、@ GeekのEnumを使用していると仮定します

Enum EDirection { RIGHT =0, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT,DOWN, DOWN_RIGHT}

これで、整数演算を使用して列挙型を計算できます(配列は必要ありません)。

EDirection angle2dir(int angle) {
    int d = ( ((angle%360)+360)%360-1)/30;
    d-=d/3; //some directions cover a 60 degree arc
    d%=8;
    //printf ("assert(angle2dir(%3d)==%-10s);\n",angle,dir2str[d]);
    return (EDirection) d;
}

@motoDrizztが指摘するように、簡潔なコードは必ずしも読み取り可能なコードであるとは限りません。それを数学として表現すると、いくつかの方向がより広い弧をカバーすることが明白になるという小さな利点があります。この方向に進みたい場合は、コードの理解に役立つアサートを追加できます。

assert(angle2dir(  0)==RIGHT     ); assert(angle2dir( 30)==RIGHT     );
assert(angle2dir( 31)==UP_RIGHT  ); assert(angle2dir( 60)==UP_RIGHT  );
assert(angle2dir( 61)==UP        ); assert(angle2dir(120)==UP        );
assert(angle2dir(121)==UP_LEFT   ); assert(angle2dir(150)==UP_LEFT   );
assert(angle2dir(151)==LEFT      ); assert(angle2dir(210)==LEFT      );
assert(angle2dir(211)==DOWN_LEFT ); assert(angle2dir(240)==DOWN_LEFT );
assert(angle2dir(241)==DOWN      ); assert(angle2dir(300)==DOWN      );
assert(angle2dir(301)==DOWN_RIGHT); assert(angle2dir(330)==DOWN_RIGHT);
assert(angle2dir(331)==RIGHT     ); assert(angle2dir(360)==RIGHT     );

アサートを追加すると、複製が追加されますが、アサートの複製はそれほど悪くありません。一貫性のないアサートがある場合は、すぐにわかります。配布する実行可能ファイルを肥大化させないように、リリースバージョンからアサートをコンパイルできます。それでも、この方法は、コードの見栄えを悪くするのではなく、コードを最適化する場合に最も適しています。


1

私はパーティーに遅れていますが、列挙型フラグと範囲チェックを使用して、きちんとしたことを行うことができます。

enum EDirection {
    RIGHT =  0x01,
    LEFT  =  0x02,
    UP    =  0x04,
    DOWN  =  0x08,
    DOWN_RIGHT = DOWN | RIGHT,
    DOWN_LEFT = DOWN | LEFT,
    UP_RIGHT = UP | RIGHT,
    UP_LEFT = UP | LEFT,

    // just so we be clear, these won't have much use though
    IMPOSSIBLE_H = RIGHT | LEFT, 
    IMPOSSIBLE_V = UP | DOWN
};

角度が絶対(0から360の間)であると仮定した場合のチェック(疑似コード):

int up    = (angle >   30 && angle <  150) * EDirection.UP;
int down  = (angle >  210 && angle <  330) * EDirection.DOWN;
int right = (angle <=  60 || angle >= 330) * EDirection.Right;
int left  = (angle >= 120 && angle <= 240) * EDirection.LEFT;

EDirection direction = (Direction)(up | down | right | left);

switch(direction){
    case RIGHT:
         // do right
         break;
    case UP_RIGHT:
         // be honest
         break;
    case UP:
         // whats up
         break;
    case UP_LEFT:
         // do you even left
         break;
    case LEFT:
         // 5 done 3 to go
         break;
    case DOWN_LEFT:
         // your're driving me to a corner here
         break;
    case DOWN:
         // :(
         break;
    case DOWN_RIGHT:
         // completion
         break;

    // hey, we mustn't let these slide
    case IMPOSSIBLE_H:
    case IMPOSSIBLE_V:
        // treat your impossible case here!
        break;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.