web-dev-qa-db-ja.com

列挙されたすべてのケースがカバーされているswitchステートメントでdefaultを使用することについてClang / LLVMが警告するのはなぜですか?

次のenumおよびswitchステートメントを検討してください。

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

私はObjective-Cプログラマーですが、より幅広い読者のためにこれを純粋なCで記述しました。

Clang/LLVM 4.1 with -Weverythingはデフォルト行で警告を出します:

すべての列挙値をカバーするスイッチのデフォルトラベル

さて、これがある理由が少しわかります。完全な世界では、引数theMaskに入力する値はenumだけなので、デフォルトは必要ありません。しかし、ハッキングが発生し、初期化されていないintが私の美しい関数にスローされた場合はどうなりますか?私の機能はライブラリのドロップとして提供され、そこに何が入るかを制御できません。 defaultの使用は、これを処理する非常に優れた方法です。

なぜLLVMの神々は、この行動を彼らの地獄のデバイスに値しないと見なすのですか?引数を確認するには、ifステートメントを前に置く必要がありますか?

35
Swizzlr

これは、clangのレポートの問題またはユーザーが保護しているバージョンのどちらにも影響を受けないバージョンです。

_void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}
_

Killianは、clangが警告を発する理由を既に説明しています。列挙型を拡張すると、デフォルトのケースに陥ってしまい、おそらく望んでいないことになります。正しいことは、デフォルトのケースを削除して、nhandled条件の警告を取得することです。

ここで、誰かが列挙の外にある値で関数を呼び出すことができるかどうか心配しています。これは、関数の前提条件を満たしていないように聞こえます。testingMask列挙からの値を期待するように文書化されていますが、プログラマーは別のものを渡しました。ですからプログラマエラーを使用してassert()(またはObjective-Cを使用していると言ったようにNSCAssert())を使用します。プログラマが間違っている場合、プログラマが間違っていることを説明するメッセージでプログラムをクラッシュさせます。

33
user4051

ここにdefaultラベルがあることは、期待していることについて混乱していることを示しています。可能なすべてのenum値を明示的に使い果たしたので、defaultは実行できない可能性があり、将来の変更から保護する必要もありません。enumの場合、コンストラクトはalreadyで警告を生成します。

したがって、コンパイラーは、すべてのベースをカバーしたがthinkingのように見えることに気づきました。 switchを期待される形式に変更するための最小限の労力を費やすことにより、コンパイラーに対して、実行しているように見えることが実際に実行していることであり、それを知っていることを示します。

9
Kilian Foth

しかし、ハッキングが発生し、初期化されていないintが私の美しい関数にスローされた場合はどうなりますか?

次に ndefined Behavior を取得し、defaultは無意味になります。これを改善するためにできることは何もありません。

より明確にさせてください。初期化されていないintが関数に渡された瞬間、それは未定義の動作です。あなたの関数は停止問題を解決でき、それは問題ではありません。 UBです。 UBが呼び出された後は、何も実行できません。

4
DeadMG

Clangは混乱しています。デフォルトのステートメントがあるため、完全に細かい慣行があります。これはdefensive programmingと呼ばれ、優れたプログラミング慣行と見なされています(1)。デスクトッププログラミングではないかもしれませんが、ミッションクリティカルなシステムで多く使用されています。

防御的プログラミングの目的は、理論的には決して発生しない予期しないエラーを検出することです。このような予期しないエラーは、必ずしもプログラマーが関数に誤った入力をしたり、「悪意のあるハッキング」であるとは限りません。可能性が高いのは、変数の破損が原因である可能性があります。バッファオーバーフロー、スタックオーバーフロー、暴走コード、および関数に関連しない同様のバグが原因の可能性があります。組み込みシステムの場合、特に外部RAM回路を使用している場合は、EMIが原因で変数が変わる可能性があります。

デフォルトのステートメント内に何を書き込むかについては、プログラムがそこに行き着いたら問題が起きていると思われる場合は、何らかのエラー処理が必要です。多くの場合、おそらく「予期しないが重要ではない」などのコメントを付けた空のステートメントを単に追加するだけで、ありそうもない状況を考えたことを示すことができます。


(1)MISRA-C:2004 15.3。

4
user29079

さらに良い:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

これは、列挙型に項目を追加するときにエラーが発生しにくくなります。 enum値を符号なしにすると、> = 0のテストをスキップできます。この方法は、列挙値にギャップがない場合にのみ機能しますが、多くの場合そうです。

4
Steve Weller

デフォルトのステートメントは必ずしも役立つとは限りません。スイッチが列挙型の上にある場合、列挙型で定義されていない値はすべて、未定義の動作を実行することになります。

ご存じのとおり、コンパイラーはそのスイッチを(デフォルトで)以下のようにコンパイルできます。

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

未定義の動作をトリガーすると、後戻りすることはできません。

2
Myself

また、すべてのケースでdefault:を使用することも好みます。私はいつものようにパーティーに遅れますが、...上に見なかった他のいくつかの考え:

  • 特定の警告(または-Werrorもスローしている場合はエラー)は-Wcovered-switch-defaultから(-Weverythingからではあるが-Wallからではない)から発生しています。道徳的な柔軟性により特定の警告をオフオフにすることができる場合(つまり、-Wallまたは-Weverythingからいくつかを削除する)、 -Wno-covered-switch-default(または-Wno-error=covered-switch-defaultを使用する場合は-Werror)をスローし、一般に他の警告では-Wno-...を使用すると不快になります。
  • gcc(およびclangのより一般的な動作)については、gccのマンページで-Wswitch-Wswitch-enum-Wswitch-defaultの(異なる)switchステートメントの列挙型の同様の状況での動作。
  • 私はこの警告の概念も好きではありませんし、その言葉遣いも好きではありません。私には、警告からの単語(「デフォルトラベル...はすべて...の値をカバーしています」)は、default:ケースが常に実行されることを示唆しています。

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }
    

    最初に読んだとき、これはあなたが遭遇していると私が思ったものです-default:またはbreak;がないため、return;ケースは常に実行されます。この概念は、(私の耳にとって)clangから生まれる他の乳母スタイル(たまに役立つが)のコメントと似ています。 foo == 1の場合、両方の関数が実行されます。上記のコードにはこの動作があります。つまり、後続のケースからコードを実行し続ける可能性がある場合にのみ、ブレークアウトに失敗します!ただし、これは問題ではないようです。

徹底的であることのリスクがある、完全性のための他のいくつかの考え:

  • ただし、この動作は、他の言語またはコンパイラでの積極的な型チェックと(より)一貫していると思います。あなたが仮説を立てるときに、いくつかの再現doesintまたは何かをこの関数に渡そうとすると、明示的に独自の特定のタイプの場合、コンパイラーは積極的な警告またはエラーによってその状況で同様にあなたを保護する必要があります。しかし、そうではありません! (つまり、少なくともgccclangenum型チェックを行わないようですが、 iccします )。 type-safetyを取得していないため、valueを取得できます上記の安全性。それ以外の場合は、TFAで提案されているように、structまたはタイプセーフを提供できるものを検討してください。
  • もう1つの回避策は、enumなどのMaskValueIllegalに新しい「値」を作成し、caseでそのswitchをサポートしないことです。それはdefault:によって(他の奇抜な値に加えて)食べられます

長寿命の防御的コーディング!

2
hoc_age

これが代替案です:
OPは、誰かがintを渡した場合にenumが期待されるケースから保護しようとしています。または、より可能性が高いのは、誰かが古いライブラリを新しいケースに新しいヘッダーを使用して新しいプログラムにリンクした場合です。

intケースを処理するようにスイッチを変更しませんか?スイッチの値の前にキャストを追加すると、警告がなくなり、デフォルトが存在する理由についてある程度のヒントが得られます。

_void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}
_

これはmuchが各値をテストするassert()よりも不快ではないこと、またはenum値の範囲が整然としていることを前提としていることがわかりましたより簡単なテストが機能するように。これは、デフォルトが正確かつ美しく行うことを行う醜い方法です。

1
Richard