私は最近、タイプミスによって引き起こされた私のコードのバグを見つけるのに少し時間を失いました:
if(a=b)
の代わりに:
if(a==b)
if
ステートメントの変数に値を割り当てたい特定のケースがあるのか、そうでない場合、コンパイラーが警告またはエラーをスローしないのか疑問に思いました。
if (Derived* derived = dynamic_cast<Derived*>(base)) {
// do stuff with `derived`
}
これはアンチパターンとして頻繁に引用されますが(「仮想ディスパッチを使用!」)、Derived
型にはBase
にはない機能があります(その結果、異なる関数になります)。これは、そのセマンティックの違いを有効にする良い方法です。
問題の構文の履歴を次に示します。
古典的なCでは、エラー処理は次のような記述で頻繁に行われました。
int error;
...
if(error = foo()) {
printf("An error occured: %s\nBailing out.\n", strerror(error));
abort();
}
または、nullポインターを返す可能性のある関数呼び出しがあるたびに、イディオムは逆方向で使用されました。
Bar* myBar;
... //in old C variables had to be declared at the start of the scope
if(myBar = getBar()) {
//do something with myBar
}
ただし、この構文は非常に危険です
if(myValue == bar()) ...
これが、多くの人々が条件内の代入を悪いスタイルと考える理由であり、コンパイラはそれについて警告し始めました(少なくとも-Wall
)。ただし、追加の括弧を追加することにより、この警告を回避できます。
if((myBar = getBar())) { //tells the compiler: Yes, I really want to do that assignment!
その後、C99が登場し、定義とステートメントを混在させることができるようになったため、多くの開発者が次のような記述を頻繁に行うようになりました。
Bar* myBar = getBar();
if(myBar) {
気まずい感じがします。これが、最新の標準が条件内の定義を許可し、これを行う簡潔でエレガントな方法を提供する理由です。
if(Bar* myBar = getBar()) {
このステートメントにはもう危険はありません。明示的に変数に型を指定し、明らかに初期化する必要があります。また、変数を定義するための余分な行(ニース)を回避します。しかし、最も重要なことは、コンパイラがこの種のバグを簡単にキャッチできることです。
if(Bar* myBar = getBar()) {
...
}
foo(myBar->baz); //compiler error
//or, for the C++ enthusiasts:
myBar->foo(); //compiler error
if
ステートメント内の変数定義がないと、この状態は検出できません。
長い答えを短くする:あなたの質問の構文は古いCの単純さと力の産物ですが、それは邪悪なので、コンパイラはそれについて警告することができます。一般的な問題を表現するのに非常に便利な方法でもあるため、同じ動作を実現するための非常に簡潔でバグに強い方法があります。そして、多くの良い、可能な用途があります。
割り当て演算子は、割り当てられた値の値を返します。だから、私はこのような状況でそれを使用するかもしれません:
if(x = getMyNumber())
次に、x
がgetMyNumber
によって返される値に割り当て、ゼロでないかどうかを確認します。
それを避けてください、私はあなたがこれを理解するのを助けるためにあなたに例を与えました。
Edit:追加ただの提案(好きかもしれません)。
avoidいくつかの拡張までは、if条件をif(NULL == ptr)
ではなくif(ptr == NULL)
として記述する必要があります。等価チェック演算子_==
_を演算子_=
_と間違えた場合、コンパイルはif(NULL = ptr)
で左辺値エラーをスローしますが、if(res = NULL)
はコンパイラによって渡されます意味ではありません)、ランタイムのコードのバグのままです。
同じ理由で、if(getMyNumber() ==x)
の代わりにif(x == getMyNumber())
と書くことを好みます。
また、この種のコードに関する 批評 も読む必要があります。
きれいなコードを書くかどうかに依存します。 Cが最初に開発されたとき、クリーンなコードの重要性は完全には認識されていませんでした。コンパイラーは非常に単純でした。今日、優秀なプログラマーがそれを行うケースは考えられません。コードが読みにくくなり、保守が難しくなります。
私は最近これが有用であるケースに遭遇したので、私はそれを投稿したいと思った。
単一のifで複数の条件をチェックし、条件のいずれかが真である場合、エラーメッセージを生成したいとします。特定の条件がエラーの原因となったエラーメッセージに含める場合は、次の操作を実行できます。
std::string e;
if( myMap[e = "ab"].isNotValid() ||
myMap[e = "cd"].isNotValid() ||
myMap[e = "ef"].isNotValid() )
{
// here, e has the key for which the validation failed
}
したがって、2番目の条件がtrueと評価される条件である場合、eは「cd」に等しくなります。これは、標準で義務付けられている||
の短絡動作によるものです(過負荷でない限り)。短絡の詳細については、 this answerを参照してください。
コンパイラが警告をスローしないのはなぜですか
一部のコンパイラwillは、通常は明示的に警告を有効にする必要がありますが、条件式の疑わしい割り当てに対して警告を生成します。
たとえば、Visual C++では、 C4706 (または一般的なレベル4の警告)を有効にする必要があります。私は通常、できるだけ多くの警告をオンにし、誤検知を避けるためにコードをより明確にします。たとえば、私が本当にこれをしたい場合:
if (x = Foo()) { ... }
それから私はそれを次のように書くでしょう:
if ((x = Foo()) != 0) { ... }
コンパイラーは明示的なテストを確認し、割り当てが意図的なものであると想定しているため、ここで誤検出の警告は表示されません。
このアプローチの唯一の欠点は、変数が条件で宣言されているときに使用できないことです。つまり、書き換えることはできません。
if (int x = Foo()) { ... }
として
if ((int x = Foo()) != 0) { ... }
構文的には、それは機能しません。そのため、警告を無効にするか、x
のスコープを厳しくする必要があります。
UPDATE:C++ 17は、ifステートメントの条件にinitステートメントを含める機能を追加しました( p0305r1 )、この問題をうまく解決します(!= 0
)。
if (x = Foo(); x != 0) { ... }
さらに、必要に応じて、x
のスコープをifステートメントのみに制限できます。
if (int x = Foo(); x != 0) { /* x in scope */ ... }
// x out of scope
if
で割り当てを行うことはかなり一般的なことですが、人々が偶然にそれを行うことも一般的です。
通常のパターンは次のとおりです。
if (int x = expensive_function_call())
{
// ...do things with x
}
アンチパターンは、あなたが誤って物に割り当てる場所です:
if (x = 1)
{
// Always true
}
else
{
// Never happens
}
定数またはconst
値を最初に置くことで、ある程度これを回避できます。そのため、コンパイラーはエラーをスローします。
if (1 = x)
{
// Compiler error, can't assign to 1
}
=
vs. ==
は、目を開発する必要があるものです。私は通常、演算子の周りに空白を置くので、longname=longername
はlongname==longername
一目ですが、=
および==
それ自体は明らかに異なります。