C++ on MSVC講習/条件分岐2 - switchすうぃっち

あらすじと概要

前回はif文を使った条件分岐について解説しました。
今回は、多分岐をより単純に書くことが出来るswitch文を解説します。

重要語

列挙型

任意の列挙子と整数値を対応させ、集合にして新しい型を作るもの

列挙子

整数値と対応させる、列挙体の値に相当するもの

::

スコープ解決演算子

switch文

整数値か列挙子に応じて、複数の場合に条件分けする文

ラベル

switch文やgoto文などから移動する時の目印

caseラベル

評価された条件に応じて条件分岐するためのラベル

defaultラベル

どのcaseラベルにも当てはまらない時のラベル

フォールスルー

switch文中で、他のラベルを跨いで順次実行がされること

break文

switch文、while文、for文で使用できる、文を脱出する文

属性

コンパイラに対して、プログラマーのより詳しい意図を伝えるもの

列挙型

列挙体は、後で解説する条件分岐に使うswitch文と合わせると便利なものです。
そのため、switch文の前に列挙型について解説します。
列挙型
#include <iostream>

int main()
{
    enum
    {
        red,
        green,
        blue
    };

    enum fruit : unsigned
    {
        apple,
        orange,
        grape,
    };

    enum class color
    {
        red = -123,
        green = 456,
        blue = 789,
    };

    color c = (color)-123;

    std::cout << red << green << blue << "\n"
        << fruit::apple << fruit::orange << fruit::grape << "\n"
        << apple << orange << grape << "\n"
        << (int)c << (int)color::green << (int)(c = color::blue) << "\n";
}

実行結果1
012
012
012
-123456789

補足

C++17以降でコンパイルしなかった場合、最後の出力が789456789となることがあります。
これは、(int)c(int)(c = color::blue)などの評価順が決まっていなかったからです。
(int)(c = color::blue)が先に実行され、(int)c789に評価される事があるのです。
しかし、<<は評価順が左から右へと固定されたため、C+17以降は-123456789になります。

解説

解説にいきます。

列挙型

列挙型は、任意の列挙子に整数値を対応させ、集合にして新しい型を作るものです。
もうすこし噛み砕くと、任意の値のみ取るような型を作る機能と言えます。
列挙型は、スコープを持たない列挙型と、持つ列挙型の2種類存在します。
ちなみに、enumいーなむstructすとらくとclassくらすと読み、structclassの違いはありません。

追加される構文の記法

(なにかしらA|なにかしらB)

なにかしらAとなにかしらBのどちらか
スコープを持たない列挙型
enum 列挙型名(opt) 型指定子(opt) { 宣言子(opt) } ;
enum 列挙型名(opt) 型指定子(opt) { 宣言子 ,(opt) } ;
スコープを持つ列挙型
enum (struct|class) 列挙型名 型指定子(opt) { 宣言子(opt) } ;
enum (struct|class) 列挙型名 型指定子(opt) { 宣言子 ,(opt) } ;
型指定子
: 整数型
宣言子
列挙子 初期化子(opt)
宣言子 , 列挙子 初期化子(opt)

型指定子

型指定子は、その列挙型の列挙子がベースにする整数型を指定することが出来ます。
宣言子は、型指定子で指定した型が扱える型の値を持たせることが出来るのです。
なお、列挙型名と列挙子は、識別子の規則で命名します。

初期化子と列挙子の値

初期化子は、= 整数値の形で、列挙子に対応させる整数値を指定出来ます。
初期化子が省略された場合、始めの宣言子の場合は0に紐づけられます。
それ以外の場合は、直前の宣言子に+1をした値が紐づけられます。
複数の宣言子で値が重複すると、バグに繋がるので、重複しないようにしましょう。

列挙子のスコープ

スコープを持たない列挙型は、列挙型が宣言されたスコープに列挙子が導入されます。
一方スコープを持つ列挙型は、列挙型自体がスコープを持ち、それに列挙子が導入されます。
そのスコープは、列挙型名を名前としたスコープで、スコープ解決演算子で参照出来ます。
スコープを持たない列挙型も、スコープは持ちませんが、列挙型名があれば参照出来ます。

スコープ解決演算子

スコープ解決演算子は::です。何らかの名前付きスコープの中を参照するために使用します。
今回の例だと、列挙型colorは、colorという名前付きスコープを持っています。
そのためcolorの列挙子は、color::redのようにアクセスできるのです。
なお、std::coutstd::cin::もスコープ解決演算子ですが、今は解説しません。

列挙子と整数型の変換

スコープを持たない列挙型は、列挙子から整数型に限って暗黙の型変換が起こります。
一方スコープを持つ列挙型は、列挙子と整数型の間では暗黙の型変換が起こりません。
なお、どちらも相互的にCスタイルのキャストかstatic_castで明示的には変換できます。
なお、列挙型はstd::coutを使った出力が標準では用意されていません。

列挙型の変数

列挙型は型なので、列挙型名を型名として、列挙型の変数を宣言することが出来ます。
当然、列挙型なので、代入できるのは列挙子のみになります。
なお、列挙型の変数はベースになる型の変数そのもので、列挙子に無い値を代入出来ます。
そのため、プログラマーが列挙子の値のみを持つことを保証しないといけません。

直接的な解説

出力の上3行は特に問題ないと思うので、下3行について解説します。
まず、変数cは、color c = (color)-123と宣言しているので、値はcolor::redです。
(int)cは、cの値をint型にキャスト、つまりcolor::redの整数値で、出力は-123です。
続いて、(int)color::greencolor::greenの整数値で、出力は456です。
最後は、(int)(c = color::blue)で、まず右でccolor::blueを代入しています。
その代入後は、(int)cと同じ意味になるので、color::blueの値789が出力となります。

switch文

それでは、switch文の解説に移ります。
switch文
#include <iostream>

int main()
{
    enum class color
    {
        red,
        green,
        blue,
    };

    color c; bool f = true;

    switch (int num; (std::cin >> num), num)
    {
        case 0: c = color::red; break;
        case 1: c = color::green; break;
        case 2: c = color::blue; break;
        default: f = false;
    }

    if (f)
    {
        std::string str;

        switch (c)
        {
            case color::red:
                {
                    str = "red";
                } [[fallthrough]];
            case color::green:
                {
                    str = str + "green";
                } [[fallthrough]];
            case color::blue:
                str = str + "blue";
                [[fallthrough]];
            default:
                {
                    std::cout << str << "\n";
                }
        }
    }
    else
    {
        std::cout << "error\n";
    }
}

実行結果例1
$ -1
error

実行結果例2
$ 0
redgreenblue

実行結果例3
$ 1
greenblue

実行結果例4
$ 2
blue

解説

解説に行きます。

switch文

switch文は、整数値か列挙子に応じて、複数の場合に条件分けをするための文です。
switch文の構文のは、条件分岐してそれぞれに処理を書く性質上、複合文が用いられます。
の中では、後述するラベルを使用して条件分岐をしてそれぞれ処理を書いていきます。
switch文
switch ( 初期化文(opt) 条件 ) 文
caseラベル
case 整数定数式 :
defaultラベル
default :

初期化文(C++17)と条件

初期化文はif文と同じで、宣言や式文などを記述することが出来ます。
一方条件はif文とは違い列挙型か整数型に評価されるものでないといけません。
なお、if文の時と同様に宣言も書くことが出来るようになっています。
もちろん、どちらで変数を宣言したとしても、switch文全体をスコープに持ちます。

ラベル

ラベルと:ころんを1個以上文の頭に付けて文をラベル付けすることが出来ます。
ラベルは、switch文や今後解説するgoto文などから移動する時の目印になります。
ラベルなので、switch文とgoto文で使用されない、順次実行時には特に何も起こしません。

caseラベル、defaultラベル

caseけーすラベルやdefaultでふぉるとラベルは、switch文のの中でのみ使用することが出来ます。
caseラベルはcaseの後に整数の定数式が必要で、主に整数のリテラルや列挙子を指定します。
同一switch文では、一意性が失われるので、複数のcaseラベルで値を被らせてはいけません。
同様の理由で、同一switch文ではdefaultラベルは1つまでにしなければいけません。

実行のされ方とcase、defualtラベル

switch文は、条件で評価された値によって、どの文から実行されるかが変化します。
条件の評価値と、caseラベルの値が一致した場合、そのラベルが付けられた文へ移動します。
一致しなかった場合、defaultラベルがあれば、そのラベルが付けられた文へ移動します。
ここで、defaultラベルが存在しなかった場合は、switch文のの部分は実行されません。
前者2つの場合は移動後、そこから順次実行されるので、他のラベルは無視されます。

fallthroughふぉーるするー

フォールスルーは、switch文のの中で、先述した「ラベルへの移動」をした後の
順次実行の時において、他のラベルを跨いで順次実行がされることを指します。
この挙動は、一般的な感覚だとまず想像しないので、バグに繋がりやすい所です。
これを回避するにはbreak文を、意図した挙動なら、fallthrough属性を付けましょう。

breakぶれいく

break文は、switch文や今後解説するwhile文やfor文で使用できる、文を脱出する文です。
の中で使用することが出来て、break文が実行されるとswitch文を終了させます。
フォールスルーを回避するためには、回避したいラベルの直前にbreak文を入れましょう。
そうすれば、次のラベルを跨ぐ前にswitch文が終了し、switch文以降が実行されます。

属性

属性は、コンパイラに対して、プログラマーのより詳しい意図を伝えるために使用します。
属性は以下のように、属性名を[[]]で囲むような構文が一番基本的な構文です。
属性が書ける場所は様々ありますが、属性ごとに制限があるので、それぞれ覚えましょう。
属性名はいくつか標準で定義され、コンパイラが独自に定義していることもあります。
なお、コンパイラが認識できない属性名は、C++17まではエラーの可能性がありましたが、
C++17以降は単純に無視されると規定され、警告はあっても、エラーにはなりません。
属性
[[ 属性名 ]]

fallthrough属性(C++17)

fallthrough属性は、フォールスルーを意図していることをコンパイラに伝える属性です。
意図したフォールスルーであることを認識したコンパイラは警告を出さなくなります。
そして指定する場所は、フォールスルーを意図しているラベル付き文の直前です。
ただし、空文、即ち;だけの文に付けるので、[[fallthrough]];と書くことになります。

ラベルとswitch文と変数

switch文の初期化文条件では、switch文全体をスコープに持つ変数を宣言できます。
しかし、switch文のの中では、そのままだと変数の宣言をすることは出来ません。
なぜなら、「ラベルへ移動する」動作によって、変数の宣言を飛び越すことがあるからです。
そのため、の中では、ラベルを跨がないスコープでしか変数を宣言出来ません。
複合文でラベルを跨がないスコープを形成し、そこで変数を宣言するくらいしか出来ませんね。

補足

宣言を飛び越すのでエラーと書いたのですが、実はエラーにならない条件があります。
その1つとして、組み込み型であれば初期化子がない宣言はエラーにならないとされています。
しかし、宣言を飛び越えるようなコードはバグやエラーと隣り合わせなのであえて書きました。

if文とswitch文を比較

switch文をif文で模倣するにはelse-ifを延々と使用する必要があるので、
if文と比べると、switch文はより多分岐を分かりやすく記述することが出来ます。
しかし、if文はboolに評価される条件式で複雑に条件を立てて場合分け出来ますが、
switch文は、純粋に整数値でしか分岐できないので、使いづらかったりします。
パターンマッチングは、C++23にinspect文で入る可能性があるので、それを期待したいです。

練習問題

今回は前回のサンプルコードを少し変えたものをswitch文にしてみましょう。

問題文

以下のコードをswitch文を用いて書き換えてください。
始めの、不正な点数の検出部分と、中学生かの判定に関しては、if文でいいことにします。
コード
コード
#include <iostream>
#include <string>

int main()
{
    if (int num; (std::cin >> num), num < 0 || num > 100)
    {
        std::cout << "受けていないですね?\n";
    }
    else
    {
        if (num >= 95)      /*評価 10 */ std::cout << "素晴らしい!\n";
        else if (num >= 75) /*評価 9,8*/ std::cout << "よくできました。\n";
        else if (num >= 65) /*評価 7  */ { std::cout << "よくがんばりました。\n"; }
        else if (num >= 55) /*評価 6  */ { std::cout << "もうすこしがんばりましょう。\n"; }
        else
        {
            if (std::string s; (std::cin >> s), s == "y")
            {
                if (num >= 45)
                {   /*評価 5  */
                    std::cout << "ぎりぎりですね。がんばりましょう。\n";
                }
                else
                {   /*赤点    */
                    std::cout << "もっとがんばりましょう。\n";
                }
            }
            else
            {
                if (num >= 45)
                {   /*評価 5  */
                    std::cout << "もうすこしがんばりましょう。\n";
                }
                else if (num >= 35)
                {   /*評価 4  */
                    std::cout << "ぎりぎりですね。がんばりましょう。\n";
                }
                else
                {   /*赤点    */
                    std::cout << "もっとがんばりましょう。\n";
                }
            }
        }
    }
}

入力
$ N A
制約
Nはintに収まる整数の値
Aは"y"もしくは"n" (ただし、0<=N<=54である場合のみ入力されます)
出力
以下の表に従って出力してください。

点数と出力の関係(中学生)

100~95, 評価 10

素晴らしい!

94~75, 評価 9~8

よくできました。

74~65, 評価 7

よくがんばりました

64~55, 評価 6

もうすこしがんばりましょう。

54~45, 評価 5

ぎりぎりですね。がんばりましょう。

44~0, 評価 4~0, 赤点

もっとがんばりましょう。

上記以外

受けていないですね?

点数と出力の関係(高校生)

100~95, 評価 10

素晴らしい!

94~75, 評価 9~8

よくできました。

74~65, 評価 7

よくがんばりました

64~45, 評価 6~5

もうすこしがんばりましょう。

44~35, 評価 4

ぎりぎりですね。がんばりましょう。

34~0, 評価 3~0, 赤点

もっとがんばりましょう。

上記以外

受けていないですね?
実行結果例1
$ -1
受けていないですね?
 
実行結果例2
$ 100
素晴らしい!
 
実行結果例3
$ 80
よくできました。
 
実行結果例4
$ 70
よくがんばりました。
 
実行結果例5
$ 60
もうすこしがんばりましょう。
 
実行結果例6
$ 50 y
ぎりぎりですね。がんばりましょう。
 
実行結果例7
$ 50 n
もうすこしがんばりましょう。
 
実行結果例8
$ 40 y
もっとがんばりましょう。
 
実行結果例9
$ 40 n
ぎりぎりですね。がんばりましょう。
 
実行結果例10
$ 30 n
もっとがんばりましょう。
 
ヒント1 評価で分類しているので、switch文にするには、粗点を+ 5してから/ 10するのがいいでしょう。
そうすれば、例えば(95 + 5) / 1010になり、(94 + 5) / 109になりますね。
ヒント2 複数の評価で同じ動作をする場合は、フォールスルーを使うといいでしょう。
回答例
回答例
#include <iostream>
#include <string>

int main()
{
    if (int num; (std::cin >> num), num < 0 || num > 100)
    {
        std::cout << "受けていないですね?\n";
    }
    else
    {
        switch (num = (num + 5) / 10)
        {
            case 6: std::cout << "もうすこしがんばりましょう。\n"; break;
            case 7: std::cout << "よくがんばりました。\n"; break;
            case 8: [[fallthrough]];
            case 9: std::cout << "よくできました。\n"; break;
            case 10: std::cout << "素晴らしい!\n"; break;
            default:
                {
                    if (std::string s; (std::cin >> s), s == "y")
                    {
                        switch (num)
                        {
                            case 5: std::cout << "ぎりぎりですね。がんばりましょう。\n"; break;
                            default: std::cout << "もっとがんばりましょう。\n";
                        }
                    }
                    else
                    {
                        switch (num)
                        {
                            case 4: std::cout << "ぎりぎりですね。がんばりましょう。\n"; break;
                            case 5: std::cout << "もうすこしがんばりましょう。\n"; break;
                            default: std::cout << "もっとがんばりましょう。\n";
                        }
                    }
                }
        }
    }
}

参照、出典

参照や出典です

参照

[enum]

https://timsong-cpp.github.io/cppwp/n4861/enum

[stmt.pre]

https://timsong-cpp.github.io/cppwp/n4861/stmt.pre

[stmt.select]

https://timsong-cpp.github.io/cppwp/n4861/stmt.select#stmt.switch

[stmt.label]

https://timsong-cpp.github.io/cppwp/n4861/stmt.label

[dcl.attr]

https://timsong-cpp.github.io/cppwp/n4861/dcl.attr

列挙宣言 - cppreference.com

https://ja.cppreference.com/w/cpp/language/enum

列挙型 [C++] | Microsoft Docs

https://docs.microsoft.com/ja-jp/cpp/cpp/enumerations-cpp?view=msvc-160

スコープを持つ列挙型 - cpprefjp C++日本語リファレンス

https://cpprefjp.github.io/lang/cpp11/scoped_enum.html

文 - cppreference.com

https://ja.cppreference.com/w/cpp/language/statements

switch 文 - cppreference.com

https://ja.cppreference.com/w/cpp/language/switch

break 文 - cppreference.com

https://ja.cppreference.com/w/cpp/language/break

属性指定子(C++11以上) - cppreference.com

https://ja.cppreference.com/w/cpp/language/attributes

属性: fallthrough (C++17以上) - cppreference.com

https://ja.cppreference.com/w/cpp/language/attributes/fallthrough

goto 文 - cppreference.com

https://ja.cppreference.com/w/cpp/language/goto

厳密な式の評価順 - cpprefjp C++日本語リファレンス

https://cpprefjp.github.io/lang/cpp17/expression_evaluation_order.html