この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。
39. 不正なコードが動作する理由
この問題は、Miranda NG プロジェクトで見つかりました。このコードにはエラーが含まれています。PVS-Studio アナライザーは、次の診断を出力します。
V502 Perhaps the ‘?:’ operator works in a different way than was expected. The ‘?:’ operator has a lower priority than the ‘|’ operator. (V502 ‘?:’ 演算子の動作が想定と異なる可能性があります。’|’ 演算子のほうが ‘?:’ よりも優先されます。)
#define MF_BYCOMMAND 0x00000000L void CMenuBar::updateState(const HMENU hMenu) const { .... ::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR, MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED); .... }
説明
これまで、プログラムの不正な動作につながる多くの問題を見てきました。ここでは、別の考えさせられるトピックについて述べたいと思います。場合によっては、完全に間違ったコードが、あらゆる予想に反して、問題なく動作することがあります。経験豊富なプログラマーにとってこれは驚くべきことではありませんが、C/C++ を習い始めたばかりの初心者は困惑させられるでしょう。ここでは、そのような例について考えます。
上記のコードでは、特定のフラグを設定して CheckMenuItem() を呼び出す必要があります。そして、bShowAvatar が true の場合は MF_BYCOMMAND と MF_CHECKED のビット単位の OR (論理和) を、false の場合は MF_BYCOMMAND と MF_UNCHECKED のビット単位の OR (論理和) を計算します。単純でしょう!
上記のコードでプログラマーは、非常に自然な三項演算子 (if-then-else の便利な省略形) を選択して以下を表現しています。
MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED
“|” 演算子の優先順位は、”?:” 演算子よりも高いため (詳細は、「C/C++ の操作の優先順位」 (英語) を参照)、一度に 2 つのエラーが発生します。
1 つ目のエラーは、条件の変更です。”dat->bShowAvatar” ではなく、”MF_BYCOMMAND | dat->bShowAvatar” になります。
2 つ目のエラーは、MF_CHECKED または MF_UNCHECKED のいずれか 1 つのフラグのみが選択されることです。MF_BYCOMMAND フラグは無視されます。
しかし、これらの問題があるにもかかわらず、コードは正しく動作します。その理由は、単に運が良かっただけです。幸運にも、MF_BYCOMMAND (英語) フラグが 0x00000000L と等しかったのです。MF_BYCOMMAND フラグが 0 と等しい場合、コードへの影響はありません。経験豊富なプログラマーの方はすでにお分かりでしょうが、初心者のために説明したいと思います。
最初に、括弧を追加した正しい式を見てみましょう。
MF_BYCOMMAND | (dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED)
マクロを数値に置き換えます。
0x00000000L | (dat->bShowAvatar ? 0x00000008L : 0x00000000L)
| 演算子のオペランドの 1 つがゼロの場合、式を簡素化できます。
dat->bShowAvatar ? 0x00000008L : 0x00000000L
不正なコードについて詳しく見てみます。
MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED
マクロを数値に置き換えます。
0x00000000L | dat->bShowAvatar ? 0x00000008L : 0x00000000L
部分式 “0x00000000L | dat->bShowAvatar” で | 演算子のオペランドの 1 つがゼロです。式を簡素化します。
dat->bShowAvatar ? 0x00000008L : 0x00000000L
結果、同じ式になり、これがエラーを含むコードが正しく動作する理由です。また 1 つプログラミングの奇蹟が起こりました。
正しいコード
正しいコードの記述方法はさまざまです。その 1 つは括弧を追加することで、別の方法として中間値を追加することもできます。古き良き if 演算子も役立ちます。
if (dat->bShowAvatar) ::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR, MF_BYCOMMAND | MF_CHECKED); else ::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR, MF_BYCOMMAND | MF_UNCHECKED);
私は、この方法でコードを修正するように主張しているわけではありません。読みやすいかもしれませんが、長いため、好みの問題です。
推奨事項
単純に、複雑な式、特に三項演算子を使用しないようにします。括弧を追加するのも忘れないでください。
「4. ?: 演算子に注意して、括弧で囲む」で述べたように、?: は非常に危険です。優先順位が非常に低いことをうっかり忘れてしまい、不正な式を記述してしまう場合があります。この演算子は、文字列を詰める場合に使用される傾向にありますが、その場合この演算子を使用しないようにします。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。