web-dev-qa-db-ja.com

三項演算子とifステートメントの可愛さ

私はいくつかのコードを閲覧していて、その中にいくつかの三項演算子が見つかりました。このコードは、私たちが使用するライブラリであり、非常に高速であることが想定されています。

そこのスペース以外のものを保存するかどうか考えています。

あなたの経験は何ですか?

26
hummingBird

パフォーマンス

三項演算子は、適切に記述された同等のif/elseステートメントとパフォーマンスに違いがあってはなりません...抽象構文ツリーで同じ表現に解決されたり、同じ最適化を受けたりする場合があります。

あなただけができること? :

定数または参照を初期化する場合、またはメンバー初期化リスト内で使用する値を算出する場合、if/elseステートメントは使用できませんが、_?_ _:_は次のようになります:

_const int x = f() ? 10 : 2;

X::X() : n_(n > 0 ? 2 * n : 0) { }
_

簡潔なコードの因数分解

_?_ _:_を使用する主な理由にはローカリゼーションが含まれ、同じステートメント/関数呼び出しの他の部分が重複して繰り返されることを回避します。次に例を示します。

_if (condition)
    return x;
else
    return y;
_

...よりも好ましい...

_return condition ? x : y;
_

...非常に経験の浅いプログラマを扱う場合、または_?_ _:_構造がノイズで失われるほど複雑な用語がある場合は、読みやすさの理由から。次のようなより複雑な場合:

_fn(condition1 ? t1 : f1, condition2 ? t2 : f2, condition3 ? t3 : f3);
_

同等のif/else

_if (condition1)
    if (condition2)
        if (condition3)
            fn(t1, t2, t3);
        else
            fn(t1, t2, f3);
    else if (condition3)
            fn(t1, f2, t3);
        else
            fn(t1, f2, f3);
else
    if (condition2)
       ...etc...
_

これは、コンパイラーが最適化する場合としない場合がある、多くの追加の関数呼び出しです。

名前付き一時ファイルは、上記のif/elseの怪物を改善できませんか?

_t1_、_f1_、_t2_などの式が冗長すぎて繰り返し入力できない場合は、名前付き一時ファイルを作成すると役立つことがありますが、次のようになります。

  • _?_ _:_に一致するパフォーマンスを得るには、同じ一時変数が呼び出される関数の2つの_std::move_パラメーターに渡される場合を除いて、_&&_を使用する必要がある場合があります。 。これはより複雑でエラーが発生しやすくなります。

  • c _?_ x _:_ ycを評価します---両方ではなくどちらかxyなので、ポインタをテストすることは安全ではありません。 t nullptrを使用する前に、フォールバック値/動作を提供します。コードはxyのどちらかの副作用のみを取得します=が実際に選択されています。名前付きテンポラリでは、初期化の中でif/elseを必要とするか、不要なコードを実行するために_?_ _:_を実行するか、必要以上にコードを実行する必要があります。

機能の違い:結果タイプの統一

考慮してください:

_void is(int) { std::cout << "int\n"; }
void is(double) { std::cout << "double\n"; }

void f(bool expr)
{
    is(expr ? 1 : 2.0);

    if (expr)
        is(1);
    else
        is(2.0);
}
_

上記の条件演算子バージョンでは、_1_はdoubleへの標準変換を受け、型は_2.0_に一致します。つまり、true/_1_でもis(double)オーバーロードが呼び出されます状況。 if/elseステートメントはこの変換をトリガーしません。true/_1_ブランチはis(int)を呼び出します。

全体的な型がvoidの式は、条件演算子でも使用できませんが、if/elseの下のステートメントでは有効です。

重点:値を必要とするアクションの前/後の値選択

異なる重点があります:

if/elseステートメントは最初に分岐を強調し、実行される処理は二次的なものですが、三項演算子はそれを使用する値の選択に対して実行される処理を強調します。

状況によっては、プログラマーの「自然な」見方をコードに反映し、理解、検証、保守を容易にする場合もあります。コードを記述するときにこれらの要素を検討する順序に基づいて、一方を他方から選択する場合があります。「何かを行う」ことを開始した場合は、いくつかの(または少数の)値のいずれかを使用して行う可能性があります。 _?_ _:_は、それを表現してコーディングの「フロー」を続行するための最も中断の少ない方法です。

50
Tony Delroy

上手...

GCCとこの関数呼び出しでいくつかのテストを行いました。

add(argc, (argc > 1)?(argv[1][0] > 5)?50:10:1, (argc > 2)?(argv[2][0] > 5)?50:10:1, (argc > 3)?(argv[3][0] > 5)?50:10:1);

結果のgcc -O3を使用したアセンブラコードには35の命令が含まれていました。

If/else +中間変数を含む同等のコードは36でした。ネストされたif/elseを使用して、3> 2> 1の事実を使用して、44を取得しました。これを個別の関数呼び出しに拡張することすらしませんでした。

今、私はパフォーマンス分析も、結果のアセンブラコードの品質チェックも行いませんでしたが、ループのないこのような単純なものでは、e.t.c。短いほど良いと思います。

結局のところ、3項演算子には何らかの価値があるようです:-)

もちろん、それはコードの速度が絶対に重要な場合だけです。 if/elseステートメントは、(c1)?(c2)?(c3)?(c4)?: 1:2:3:4のようなものよりもネストされた方がはるかに読みやすくなります。そして、関数の引数として巨大な式を持つことはnot funです。

また、ネストされた3項式を使用すると、コードをリファクタリングしたり、条件に一連の便利なprintfs()を配置してデバッグしたりするのが非常に難しくなります。

8
thkala

私の見解では、プレーンifステートメントよりも三項演算子の唯一の潜在的な利点は、初期化に使用できることです。これは、constで特に役立ちます。

例えば。

const int foo = (a > b ? b : a - 10);

関数calを使用しないと、if/elseブロックでこれを行うことはできません。このようなconstのケースがたくさんある場合は、if/elseを使用した割り当てよりもconstを適切に初期化することで小さな利益が得られる場合があります。測定してください!たぶん測定できません。私がこれを行う傾向があるのは、constをマークすることにより、後で何かをすると、修正済みと思っていたものを誤って変更する可能性がある/変更する可能性があることをコンパイラが知っているためです。

事実上、私が言っていることは、3項演算子はconst-correctnessにとって重要であり、constの正確さは次のような習慣になることです。

  1. これにより、コンパイラーが間違いを見つけやすくなるため、時間を大幅に節約できます。
  2. これにより、コンパイラーが他の最適化を適用できる可能性があります
7
Flexo

実際には、「if-else」式を優先して「if-else」式を優先する言語が多数ある場合、必須の2つの違いがあると想定します(この場合、3項演算子さえもない可能性があり、これはもう必要ありません)

想像してみてください:

x = if (t) a else b

とにかく、三項演算子は一部の言語(C、C#、C++、Javaなど)の式ですnotには「if-else」式があり、したがって異なる役割を果たします)あります。

3
user166390

パフォーマンスの観点からそれについて心配しているのであれば、2つの間に違いがあったとしても、私は非常に驚きます。

ルックアンドフィールの観点からは、主に個人の好みによるものです。条件が短く、true/falseの部分が短い場合、3項演算子は問題ありませんが、if/elseステートメントの方が長い方が良い傾向があります(私の意見では)。

1
Sean