Clang / LLVMが、列挙されたすべてのケースが含まれるswitchステートメントでデフォルトを使用することについて警告するのはなぜですか?


34

次の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でこれを書いています。

-Weverythingを使用したClang / LLVM 4.1では、デフォルトの行で警告が表示されます。

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

今、私はこれがなぜあるのかを見ることができます:完全な世界では、引数に入力する値theMaskは列挙型のみであるため、デフォルトは必要ありません。しかし、ハックがやって来て、初期化されていないintを美しい関数に投げ込んだらどうなるでしょうか?私の機能はライブラリのドロップとして提供され、そこに何が入るかを制御することはできません。を使用することdefaultは、これを処理する非常に適切な方法です。

なぜLLVMの神は、この振る舞いを地獄のデバイスにふさわしくないと考えているのでしょうか?引数をチェックするifステートメントをこれに先行させる必要がありますか?


1
私の理由は-Weverythingは自分自身をより良いプログラマにすることです。NSHipsterが言うように"Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode."
Swizzlr

2
-Weverything役に立つかもしれませんが、コードを変更しすぎて対処できないように注意してください。これらの警告のいくつかは、価値がないだけでなく、逆効果であるため、オフにするのが最適です。(実際、それはユースケースです-Weverything。まず始めて、意味をなさないものをオフにします。)
スティーブンフィッシャー

後世の警告メッセージについて少し詳しく教えてもらえますか?通常、彼らにはそれ以上のものがあります。
スティーブンフィッシャー

答えを読んだ後、私はまだあなたの最初の解決策を好みます。デフォルトステートメントIMHOの使用法は、回答に示された代替案よりも優れています。ほんの小さなメモ:答えは本当に良くて有益であり、問​​題を解決するクールな方法を示しています。
bbaja42

@StevenFisherそれは全体の警告でした。また、後で列挙を変更するとKillianが指摘したように、有効な値がデフォルトの実装に渡される可能性が開かれます。良いデザインパターンのようです(そのような場合)。
Swizzlr

回答:


31

以下に、clangのレポートの問題も、保護している問題も発生していないバージョンを示します。

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

Killianは、clangが警告を発する理由をすでに説明しています。enumを拡張した場合、デフォルトのケースに陥ります。正しいことは、デフォルトのケースを削除し、未処理の状態に関する警告を取得することです。

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


6
+1。小さな変更として、各ケースを持ち、末尾にreturn;を追加するassert(false);ことを好みます(イニシャルassert()とに有効な列挙をリストすることで自分自身を繰り返すのではなくswitch)。
ジョシュケリー

1
間違った列挙型が愚かなプログラマーではなくメモリ破損バグによって渡された場合はどうなりますか?ドライバーがトヨタのブレークを押して、バグが列挙型を破損した場合、ブレーク処理プログラムがクラッシュして燃え、ドライバーにテキストが表示されるはずです:「プログラマーは間違っています、悪いプログラマーです! 「。崖の端を運転する前に、これがユーザーにどのように役立つかはわかりません。

2
@Lundinは、OPが何をしているのか、それともあなたが構築したばかりの無意味な理論上のエッジケースですか?とにかく、「メモリ破損のバグ」[i]はプログラマーのエラーであり、[ii]は(少なくとも質問で述べられている要件がなければ)意味のある方法で続行できるものではありません。

1
@GrahamLee予想外のエラーを見つけたとき、「横になって死ぬ」ことは必ずしも最良の選択肢ではないと言っているだけです。

1
アサーションとケースが誤って同期しなくなるという新しい問題があります。
user253751

9

持ってdefaultここにラベルすることは、あなたが期待しているかについて混乱していることの指標です。あなたはすべての可能使い果たしてしまったのでenum、明示的な値を、defaultおそらく実行できない、とあなたはそれが将来のを防ぐために必要はありませんあなたが拡張場合ので、いずれかの変更enum、その後、構築物はしまうすでに警告を生成します。

そのため、コンパイラは、すべてのベースをカバーしたが、カバーしていないと考えているように見えることに気付き、それは常に悪い兆候です。switch予想される形式に変更するために最小限の労力をかけることで、コンパイラに、あなたがしているように見えることが実際にあなたがしていることであり、それを知っていることを示します。


1
しかし、私の懸念は汚れた変数であり、それに対する保護です(前述のように、初期化されていないintなど)。このような状況では、switchステートメントがデフォルトに達する可能性があるように思えます。
Swizzlr

Objective-Cの定義を暗記していませんが、typdefされた列挙型がコンパイラによって強制されると想定しました。言い換えれば、「初期化されていない」値は、プログラムがすでに未定義の動作を示している場合にのみメソッドに入ることができ、コンパイラがそれを考慮しないことは完全に合理的であると思います。
キリアンフォス

3
@KilianFothいいえ、そうではありません。Objective-C列挙型は、Java列挙型ではなく、C列挙型です。基礎となる整数型の値は、関数の引数に存在する可能性があります。

2
また、整数から列挙型を実行時に設定できます。そのため、考えられる任意の値を含めることができます。

OPは正しいです。testingMask m; myFunction(m); ほとんどの場合、デフォルトのケースにヒットします。
マシュージェームスブリッグス

4

Clangは混乱しており、デフォルトのステートメントがあり、完全に素晴らしいプラクティスがあります。これは防御プログラミングとして知られており、優れたプログラミングプラクティスと見なされています(1)。デスクトッププログラミングではないかもしれませんが、ミッションクリティカルなシステムでよく使用されます。

防御的プログラミングの目的は、理論上は決して起こらない予期しないエラーをキャッチすることです。このような予期しないエラーは、必ずしもプログラマーが関数に誤った入力を与えたり、「悪意のあるハック」を行ったりするわけではありません。おそらく、変数の破損が原因である可能性があります。バッファオーバーフロー、スタックオーバーフロー、ランナウェイコード、および関数に関連しない同様のバグが原因です。また、組み込みシステムの場合、特に外部RAM回路を使用している場合、EMIにより変数が変更される可能性があります。

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


(1)MISRA-C:2004 15.3。


1
普通のPCプログラマーは通常、防御的プログラミングの概念を完全に異質なものと見なします。なぜなら、彼らのプログラムの見方は、理論上間違っていることは実際にはうまくいかない抽象的なユートピアだからです。したがって、あなたが尋ねる人に応じて、あなたの質問に対する非常に多様な答えを得るでしょう。

3
Clangは混乱していません。すべての警告を提供するように明示的に要求されています。これには、文体的な警告など、必ずしも有用ではない警告も含まれます。(個人的にこの警告を選択するのは、enumスイッチにデフォルトが必要ないためです。それらを使用すると、enum値を追加して処理しなくても警告が表示されません。このため、常に列挙外の不正な値の場合。)
イェンスエイトン

2
@JensAyton混乱しています。switchステートメントにデフォルトステートメントがない場合、すべてのプロフェッショナルな静的アナライザーツールとすべてのMISRA準拠コンパイラーは警告を出します。自分で試してみてください。

4
MISRA-CへのコーディングはCコンパイラの唯一の有効な使用法ではなく、また、特定のユースケースに適した選択ではなく、すべての警告を-Weverythingオンにします。好みに合わないということは、混乱と同じことではありません。
イェンスエイトン

2
@Lundin:MISRA-Cに固執してもプログラムが魔法のように安全でバグのないものになることはないと確信しています...そして、MISRAのことを聞いた。実際、それは誰かの(大衆的であるが、議論の余地のないものではない)意見にすぎず、そのルールのいくつかはarbitrary意的であり、時には設計された文脈の外では有害でさえあると感じる人々がいます。
cHao

4

より良い:

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
    }
};

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


2
これは、単にそのタイプに含まれたくない値でタイプを汚染しています。ここの古き良きWTFと類似点があります:thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto

4

しかし、ハックがやって来て、初期化されていないintを美しい関数に投げ込んだらどうなるでしょうか?

その後、Undefined Behaviorを取得しますdefaultが、意味がありません。これを改善するためにできることは何もありません。

もっとはっきりさせてください。誰かがintあなたの関数に初期化されていないものを渡す瞬間、それは未定義の振る舞いです。あなたの関数は停止問題を解決できますが、それは問題ではありません。UBです。UBが呼び出されたら、おそらくできることは何もありません。


3
黙って無視した場合、ログに記録するか、例外をスローしますか?
jgauffin

3
実際、スイッチにデフォルトのステートメントを追加し、エラーを適切に処理するなど、できることがたくさんあります。これは、防御的プログラミングとして知られています。それは唯一の機能誤って入力するだけでなく、バッファオーバーフローによって引き起こされるさまざまなバグ、スタックオーバーフローなどの暴走コードに対するダムプログラマの取り扱いに対して保護されませんなど

1
いいえ、何も処理しません。UBです。プログラムには、関数が入力された瞬間にUBがあり、関数本体はそれに対して何もできません。
DeadMG

2
@DeadMG enumは、実装内の対応する整数型が持つ可能性のある任意の値を持つことができます。未定義のものはありません。初期化されていない自動変数の内容にアクセスするのは確かにUBですが、「悪意のあるハック」(これは何らかのバグである可能性が高い)は、値の1つではなくても、明確に定義された値を関数に投げる可能性がありますenum宣言にリストされています。

2

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

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

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

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


2
まず、未定義の動作ではなく、未指定の値です。これは、コンパイラーがより便利な他の値を想定する可能性があることを意味しますが、月をグリーンチーズなどに変えない場合があります。Cでは、enum型の値の範囲は、その基になる整数型の値の範囲と同じです(これは実装定義であるため、一貫性が必要です)。
イェンスエイトン

1
ただし、enum変数のインスタンスと列挙定数は、必ずしも同じ幅と符号付きである必要はなく、両方ともimpl.definedです。しかし、Cのすべての列挙型は、対応する整数型とまったく同じように評価されるため、未定義の動作や指定されていない動作さえありません。

2

また、私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-defaultswitch文で列挙型の同様の状況にあるため(別の)行動を。
  • 私はまた、この警告の概念が好きではなく、その言葉遣いも好きではありません。私には、警告からの言葉(「デフォルトラベル...すべての...値をカバー」)は、次のdefault:ようにケースが常に実行されることを示唆しています。

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

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

物足りなさの危険にさらされて、完全性のためのいくつかの他の考え:

  • ただし、この動作は、他の言語またはコンパイラでの積極的な型チェックと(より)一貫していると思います。あなたが仮説として、何らかの調査intこの関数に何かを渡すことを試みた場合、それは明示的にあなた自身の特定の型を消費することを意図しています、コンパイラは積極的な警告またはエラーでそのような状況であなたを等しく保護する必要があります。しかし、そうではありません!(つまり、少なくともタイプチェックgccclang行わないように見えますが、enumiccありません)。型の安全性を取得していないため、上記のように値の安全性を取得できます。そうでない場合は、TFAで提案されているように、structタイプセーフを提供できるa または何かを検討してください。
  • 別の回避策は、新しい「値」を作成することです enumなどMaskValueIllegal、およびそれをサポートしていないcaseあなたにswitch。それはdefault:(他の奇抜な価値に加えて)食べられるだろう

長生きする防御コーディング!


1

別の提案があります:
OPはintenumが期待される場所に誰かが通過する場合から保護しようとしています。または、より多くの場合、誰かがより多くのケースで新しいヘッダーを使用して古いライブラリを新しいプログラムにリンクした場合です。

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
    }
}

これは、可能な値のそれぞれをテストしたり、列挙値の範囲が整頓されていて、より簡単なテストが機能すると仮定したりするよりも、はるかに不快ではありません。これは、デフォルトが正確かつ美しく行うことを行うugい方法です。assert()


1
この投稿は読みにくい(テキストの壁)。ですが、あなたは気編集をより良い形にそれをINGの?
ブヨ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.