有効ですが、スイッチケースの価値のない構文?


207

少しタイプミスをして、私は誤ってこの構成を見つけました:

int main(void) {
    char foo = 'c';

    switch(foo)
    {
        printf("Cant Touch This\n");   // This line is Unreachable

        case 'a': printf("A\n"); break;
        case 'b': printf("B\n"); break;
        case 'c': printf("C\n"); break;
        case 'd': printf("D\n"); break;
    }

    return 0;
}

と思われるprintfの一番上のswitch文が有効でなく、完全に到達不能です。

到達できないコードについての警告さえないまま、クリーンなコンパイルを取得しましたが、これは無意味なようです。

コンパイラはこれに到達できないコードとしてフラグを立てるべきですか?
これは何か目的を果たしますか?


51
GCCにはこのための特別なフラグがあります。それは-Wswitch-unreachable
Eli Sadoff 2017年

57
「これはなんらかの目的に役立ちますか?」さて、gotoそうでなければ到達できない部分に出入りすることができます。これは、さまざまなハックに役立つ場合があります。
HolyBlackCat 2017年

12
@HolyBlackCat到達できないすべてのコードについてはそうではありませんか?
Eli Sadoff 2017年

28
@EliSadoff確かに。それは特別な目的には役立たないと思います。禁止する理由がないからといって許されるのではないでしょうか。結局のところ、複数のラベルをswitch持つ条件goto付きです。gotoラベルで満たされた通常のコードブロックと同じように、本文に同じ制限があります。
HolyBlackCat 2017年

16
@MooingDuckの例がDuffのデバイス(en.wikipedia.org/wiki/Duff's_device)のバリアントであることを指摘する価値がある
Michael Anderson

回答:


226

おそらく最も有用ではありませんが、完全に価値がないわけではありません。これを使用して、switchスコープ内で使用可能なローカル変数を宣言できます。

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

標準(N1579 6.8.4.2/7)には次のサンプルがあります。

例人工プログラムフラグメント

switch (expr)
{
    int i = 4;
    f(i);
case 0:
    i = 17;
    /* falls through into default code */
default:
    printf("%d\n", i);
}

識別子がi(ブロック内で)自動ストレージ期間で存在するが初期化されていないオブジェクト。したがって、制御式の値がゼロ以外の場合、printf関数の呼び出しは不確定値にアクセスします。同様に、関数の呼び出しにf到達できません。

PS BTW、サンプルは有効なC ++コードではありません。その場合(N4140 6.7/3、私の強調):

自動ストレージ期間を持つ変数がスコープ内にないポイントからスコープ内にあるポイントに90ジャンプするプログラムは、変数がスカラータイプ、自明なデフォルトコンストラクターおよび自明なデストラクタを持つクラスタイプでない限り、形式が正しくありません。これらのタイプのいずれかのcv修飾バージョン、または前述のタイプのいずれかの配列であり、初期化子なしで宣言されています(8.5)。


90)switchステートメントの条件からケースラベルへの移行は、この点でジャンプと見なされます。

したがって、で置き換えるint i = 4;int i;、有効なC ++になります。


3
「しかし、決して初期化されない...」はi4に初期化されているように見えますが、何が欠けていますか?
矢野

7
変数がの場合、staticゼロに初期化されるため、これも安全に使用できます。
ロイセンコ

23
@yano私たちは常にi = 4;初期化を飛び越えるので、それは決して起こりません。
AlexD 2017年

12
もちろんです!...質問の全体的なポイント...ねえ。この愚かさを
消し

1
いいね!時々case、内部に一時変数が必要で、常にそれぞれに異なる名前を使用caseするか、スイッチの外部で定義する必要がありました。
SJuan76 2017年

98

これは何か目的に役立ちますか?

はい。ステートメントの代わりに、最初のラベルの前に宣言を置くと、これは完全に理にかなっています。

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

宣言とステートメントのルールはブロック全体で共有されるため、ステートメントを許可することを許可するのと同じルールです。


同様に言及する価値があるのは、最初のステートメントがループ構造の場合、ケースのラベルがループ本体に表示される可能性があることです。

switch (i) {
  for (;;) {
    f();
  case 1:
    g();
  case 2:
    if (h()) break;
  }
}

より読みやすい方法でコードを記述しないでください。ただし、コードは完全に有効であり、f()呼び出しは到達可能です。


@MatthieuM Duffのデバイスにはループ内にケースラベルがありますが、ループの前のケースラベルで始まります。

2
興味深い例に賛成するのか、実際のプログラムでこれを書くことの完全な狂気に反対するのかわからない:) 奈落の底に飛び込み、ワンピースに戻ってきて、おめでとうございます。
Liviu T.

@ChemicalEngineer:コードがループの一部である場合、それがDuffのデバイスに{ /*code*/ switch(x) { } }あるように、見た目はきれいになるかもしれませんが、それも間違っています。
Ben Voigt 2017年

39

ダフのデバイスと呼ばれるこれの有名な使用があります。

int n = (count+3)/4;
switch (count % 4) {
  do {
    case 0: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
  } while (--n > 0);
}

ここでは、が指すバッファーをが指すバッファーにコピーfromtoます。countデータのインスタンスをコピーします。

do{}while()ステートメントは、最初の前に始まりcase、ラベル、およびcaseラベルは内に埋め込まれていますdo{}while()

これにより、do{}while()ループの最後にある条件付き分岐の数が約4倍減少します(この例では、定数は任意の値に調整できます)。

現在、オプティマイザはこれを行うことができます(特に、ストリーミング/ベクトル化された命令を最適化している場合)が、プロファイルに基づく最適化がなければ、ループが大きくなるかどうかはわかりません。

一般に、変数宣言はそこで発生し、すべての場合に使用できますが、スイッチの終了後はスコープ外になります。(初期化はスキップされます)

さらに、スイッチ固有ではない制御フローを使用すると、上記のように、またはを使用して、スイッチブロックのそのセクションに移動できますgoto


2
もちろん、これは最初のケースの上にステートメントを許可しなくても可能です。順序do {case 0:重要ではないので、どちらも最初のにジャンプターゲットを配置するのに役立ち*to = *from++;ます。
Ben Voigt 2017年

1
@BenVoigt置くdo {方が読みやすいと私は主張します。はい、Duff's Deviceの可読性についての議論は愚かで無意味であり、おそらく発狂する簡単な方法です。
モニカの訴訟に資金

1
@QPaysTaxes CのSimon Tathamのコルーチンをチェックしてください。またはそうでないかもしれません。
JonasSchäfer2017年

@JonasSchäfer面白いことに、これは基本的にC ++ 20コルーチンが実行することです。
Yakk-Adam Nevraumont

15

Linuxでgccを使用している場合、4.4以前のバージョンを使用していると警告が表示されます。

-Wunreachable-codeオプションは、gcc 4.4以降で削除されました。


直接問題を経験したことは常に役立ちます!
16トン2017年

@JonathanLeffler:選択された特定の最適化パスのセットの影響を受けやすいgcc警告の一般的な問題は依然として残念ながら真実であり、ユーザーエクスペリエンスが低下します。クリーンなデバッグビルドの後に失敗したリリースビルドが続くのは本当に厄介です:/
Matthieu M.

@MatthieuM .:このような警告は、意味解析ではなく文法解析を伴う場合に非常に簡単に検出できるように思われます(たとえば、コードは、両方のブランチにリターンがある「if」の後に続く)。警告。一方、デバッグビルドに含まれていないリリースビルドでエラーや警告を表示すると便利な場合があります(他に何もない場合、デバッグのためにハックがクリーンアップされるはずの場所)リリース)。
スーパーキャット2017年

1
@MatthieuM .:特定の変数がコード内のどこかで常に偽になることを発見するために重要な意味分析が必要な場合、その変数が真であることを条件とするコードは、そのような分析が実行された場合にのみ到達不可能であることがわかります。完全でなければなりませんので、一方で、私は、そのようなコードではなく、異なる構文的に到達不能コードについての警告から到達不可能だったとの通知を検討する通常の様々な条件がいくつかのプロジェクト構成ではなく、他の人と可能性が持っています。それは
時々

1
...一部の構成が他の構成よりも大きなコードを生成する理由を知るため(たとえば、コンパイラがある条件では不可能と見なす条件があるため)だからといって、最適化できるコードに「誤り」があるとは限りませんいくつかの構成でそのファッション。
スーパーキャット2017年

11

変数宣言だけでなく、高度なジャンプも可能です。スパゲッティコードになりにくい場合にのみ、それをうまく利用できます。

int main()
{
    int i = 1;
    switch(i)
    {
        nocase:
        printf("no case\n");

        case 0: printf("0\n"); break;
        case 1: printf("1\n"); goto nocase;
    }
    return 0;
}

プリント

1
no case
0 /* Notice how "0" prints even though i = 1 */

switch-caseは最速の制御フロー節の1つであることに注意してください。そのため、プログラマにとって非常に柔軟である必要があり、このようなケースが発生する場合があります。


そして、の違いは何であるnocase:とはdefault:
i486 '20 / 01/17

@ i486 i=4トリガーされない場合nocase
Sanchke Dellowar 2017年

@SanchkeDellowarそれが私の意味です。
njzk2 2017年

なぜケース1をケース0の前に置き、フォールスルーを使用するのではなく、なぜそれを行うのでしょうか?
JonasSchäfer2017年

@JonasWielickiこの目的では、それを行うことができます。しかし、このコードは何ができるかの単なる表示ケースです。
Sanchke Dellowar 2017年

11

switchステートメント内のコード、またはcase *:このコード内のラベルの配置場所には実質的に構造上の制限がないことに注意してください*。これにより、duffのデバイスのようなプログラミングトリックが可能になり、その実装の1つは次のようになります。

int n = ...;
int iterations = n/8;
switch(n%8) {
    while(iterations--) {
        sum += *ptr++;
        case 7: sum += *ptr++;
        case 6: sum += *ptr++;
        case 5: sum += *ptr++;
        case 4: sum += *ptr++;
        case 3: sum += *ptr++;
        case 2: sum += *ptr++;
        case 1: sum += *ptr++;
        case 0: ;
    }
}

ご覧のとおり、switch(n%8) {case 7:ラベルの間のコードは確実に到達可能です...


* のようsupercatありがたいコメントで指摘:C99ので、どちらgotoもラベル(それはcase *:ラベルかどうかは)VLA宣言が含まれている宣言の範囲内で表示されることがあります。したがって、ラベルの配置に構造上の制限がないと言うのは正しくありませんcase *:。ただし、duffのデバイスはC99標準よりも古く、VLAに依存していません。それにもかかわらず、私はこれのために最初の文に「事実上」を挿入することを余儀なくされました。


可変長配列の追加により、それらに関連する構造上の制限が課せられました。
スーパーキャット2017年

@supercatどのような制限がありますか?
cmaster-モニカを2017年

1
a gotoswitch/case/defaultlabelも、可変的に宣言されたオブジェクトまたはタイプのスコープ内に表示できません。つまり、ブロックに可変長配列のオブジェクトまたは型の宣言が含まれている場合、それらの宣言の前にラベルを付ける必要があります。標準には、VLA宣言のスコープがswitchステートメントの全体にまで及ぶ場合があることを示唆する、わかりにくい言い回しがあります。それに関する私の質問については、stackoverflow.com / questions / 41752072 /…を参照してください。
スーパーキャット2017年

@supercat:あなたはその言い回しを誤解しただけです(私が推測したのは、あなたが質問を削除した理由です)。VLAを定義できるスコープに要件を課します。その範囲を拡張するのではなく、特定のVLA定義を無効にするだけです。
キース・トンプソン

@KeithThompson:ええ、私はそれを誤解していました。脚注で現在の時制を奇妙に使用すると混乱を招き、この概念は禁止事項としてより適切に表現できたと思います。そのVLA宣言の範囲」。
スーパーキャット2017

10

警告を生成するために必要なgccオプション -Wswitch-unreachableに関連する答えを得ました。この答えは、使いやすさ / 価値のある部分について詳しく説明することです。

C11§6.8.4.2の章から直接引用(強調は私のもの

switch (expr)
{
int i = 4;
f(i);
case 0:
i = 17;
/* falls through into default code */
default:
printf("%d\n", i);
}

識別子がi(ブロック内で)自動ストレージ期間で存在するが、初期化されていないオブジェクト。したがって、制御式の値がゼロ以外の場合、printf 関数の呼び出しは不確定値にアクセスします。同様に、関数の呼び出しにf到達できません。

これは非常に自明です。これを使用して、switchステートメントスコープ内でのみ使用できるローカルスコープの変数を定義できます。


9

それで「ループと半分」を実装することは可能ですが、それを行うための最良の方法ではないかもしれません。

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));

@RichardIIそれは駄洒落か何ですか?説明してください。
ダンシア2017年

1
@Dancia彼のは、これはかなり明確であることを言っていない。このような何かを行うための最善の方法、および控えめのようなものである「いない可能性があります」。
モニカの訴訟に資金
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.