プログラミング、リファクタリング、そしてすべてにおける究極の疑問: No. 39

同カテゴリーの次の記事

インテル® SPMD コンパイラーから学べること

この記事は、インテル® デベロッパー・ゾーンに公開されている「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_BYCOMMANDMF_CHECKED のビット単位の OR (論理和) を、false の場合は MF_BYCOMMANDMF_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. ?: 演算子に注意して、括弧で囲む」で述べたように、?: は非常に危険です。優先順位が非常に低いことをうっかり忘れてしまい、不正な式を記述してしまう場合があります。この演算子は、文字列を詰める場合に使用される傾向にありますが、その場合この演算子を使用しないようにします。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事

  • Parallel Universe マガジンParallel Universe マガジン Parallel Universe へようこそ。 米国インテル社が四半期に一度オンラインで公開しているオンラインマガジンです。インテルの技術者によるテクノロジーの解説や、最新ツールの紹介など、並列化に関する記事を毎号掲載しています。第1号からのバックナンバーを PDF 形式で用意しました、ぜひご覧ください。 12 […]
  • 比較関数の罠比較関数の罠 この記事は、インテル® デベロッパー・ゾーンに公開されている「The Evil within the Comparison Functions」の日本語参考訳です。 この記事の PDF […]
  • ファクトシート: oneAPIファクトシート: oneAPI この記事は、インテル ニュースルームに公開されている「Fact Sheet: oneAPI」の日本語参考訳です。 この記事の PDF 版はこちらからご利用になれます。 oneAPI とは? oneAPI […]
  • 並列プログラミングにおけるロックの効率的な使用並列プログラミングにおけるロックの効率的な使用 この記事は、インテル® ソフトウェア・ネットワークに掲載されている「Using Locks Effectively in Parallel Programming […]
  • インテル® IPP サンプル – エラーの修正インテル® IPP サンプル – エラーの修正 この記事は、インテル® ソフトウェア・ネットワークに掲載されている「Intel IPP Samples for Windows - error correction」の日本語参考訳です。 この記事は、PVS-Studio を使用することでプログラムがどのように安全になるかを説明した記事の 1 […]