C++ on MSVC講習/条件分岐2 - switch ( すうぃっち ) 文
あらすじと概要
前回はif文を使った条件分岐について解説しました。 今回は、多分岐をより単純に書くことが出来るswitch文を解説します。
重要語
列挙型
任意の列挙子と整数値を対応させ、集合にして新しい型を作るもの
列挙子
整数値と対応させる、列挙体の値に相当するもの
::
スコープ解決演算子
switch文
整数値か列挙子に応じて、複数の場合に条件分けする文
ラベル
switch文やgoto文などから移動する時の目印
caseラベル
評価された条件
に応じて条件分岐するためのラベル
defaultラベル
どのcaseラベルにも当てはまらない時のラベル
フォールスルー
switch文中で、他のラベルを跨いで順次実行がされること
break文
switch文、while文、for文で使用できる、文を脱出する文
属性
コンパイラに対して、プログラマーのより詳しい意図を伝えるもの
列挙型
列挙体は、後で解説する条件分岐に使うswitch文と合わせると便利なものです。 そのため、switch文の前に列挙型について解説します。
列挙型
COPY
#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
COPY
012 012 012 -123456789
補足
C++17以降でコンパイルしなかった場合、最後の出力が789456789
となることがあります。 これは、(int)c
や(int)(c = color::blue)
などの評価順が決まっていなかったからです。(int)(c = color::blue)
が先に実行され、(int)c
が789
に評価される事があるのです。 しかし、<<
は評価順が左から右へと固定されたため、C+17以降は-123456789
になります。
解説
解説にいきます。
列挙型
列挙型は、任意の列挙子に整数値を対応させ、集合にして新しい型を作るものです。 もうすこし噛み砕くと、任意の値のみ取るような型を作る機能と言えます。 列挙型は、スコープを持たない列挙型と、持つ列挙型の2種類存在します。 ちなみに、enum ( いーなむ )
、struct ( すとらくと )
、class ( くらす )
と読み、struct
とclass
の違いはありません。
追加される構文の記法
(なにかしらA|なにかしらB)
なにかしらAとなにかしらBのどちらか
スコープを持たない列挙型
COPY
enum
列挙型名(opt) 型指定子(opt) {
宣言子(opt) }
;
enum
列挙型名(opt) 型指定子(opt) {
宣言子 ,
(opt) }
;
スコープを持つ列挙型
COPY
enum
(struct
|class
) 列挙型名 型指定子(opt) {
宣言子(opt) }
;
enum
(struct
|class
) 列挙型名 型指定子(opt) {
宣言子 ,
(opt) }
;
宣言子
COPY
列挙子 初期化子(opt) 宣言子 ,
列挙子 初期化子(opt)
型指定子
型指定子は、その列挙型の列挙子がベースにする整数型を指定することが出来ます。 宣言子は、型指定子で指定した型が扱える型の値を持たせることが出来るのです。 なお、列挙型名と列挙子は、識別子の規則で命名します。
初期化子と列挙子の値
初期化子は、= 整数値
の形で、列挙子に対応させる整数値を指定出来ます。 初期化子が省略された場合、始めの宣言子の場合は0
に紐づけられます。 それ以外の場合は、直前の宣言子に+1
をした値が紐づけられます。 複数の宣言子で値が重複すると、バグに繋がるので、重複しないようにしましょう。
列挙子のスコープ
スコープを持たない列挙型は、列挙型が宣言されたスコープに列挙子が導入されます。 一方スコープを持つ列挙型は、列挙型自体がスコープを持ち、それに列挙子が導入されます。 そのスコープは、列挙型名を名前としたスコープで、スコープ解決演算子で参照出来ます。 スコープを持たない列挙型も、スコープは持ちませんが、列挙型名があれば参照出来ます。
スコープ解決演算子
スコープ解決演算子は::
です。何らかの名前付きスコープの中を参照するために使用します。 今回の例だと、列挙型color
は、color
という名前付きスコープを持っています。 そのためcolor
の列挙子は、color::red
のようにアクセスできるのです。 なお、std::cout
やstd::cin
の::
もスコープ解決演算子ですが、今は解説しません。
列挙子と整数型の変換
スコープを持たない列挙型は、列挙子から整数型に限って暗黙の型変換が起こります。 一方スコープを持つ列挙型は、列挙子と整数型の間では暗黙の型変換が起こりません。 なお、どちらも相互的にCスタイルのキャストかstatic_castで明示的には変換できます。 なお、列挙型はstd::cout
を使った出力が標準では用意されていません。
列挙型の変数
列挙型は型なので、列挙型名を型名として、列挙型の変数を宣言することが出来ます。 当然、列挙型なので、代入できるのは列挙子のみになります。 なお、列挙型の変数はベースになる型の変数そのもので、列挙子に無い値を代入出来ます。 そのため、プログラマーが列挙子の値のみを持つことを保証しないといけません。
直接的な解説
出力の上3行は特に問題ないと思うので、下3行について解説します。 まず、変数c
は、color c = (color)-123
と宣言しているので、値はcolor::red
です。(int)c
は、c
の値をint型にキャスト、つまりcolor::red
の整数値で、出力は-123
です。 続いて、(int)color::green
はcolor::green
の整数値で、出力は456
です。 最後は、(int)(c = color::blue)
で、まず右でc
にcolor::blue
を代入しています。 その代入後は、(int)c
と同じ意味になるので、color::blue
の値789
が出力となります。
switch文
それでは、switch文の解説に移ります。
switch文
COPY
#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" ;
}
}
実行結果例2
COPY
$ 0 redgreenblue
解説
解説に行きます。
switch文
switch文は、整数値か列挙子に応じて、複数の場合に条件分けをするための文です。 switch文の構文の文
は、条件分岐してそれぞれに処理を書く性質上、複合文が用いられます。文
の中では、後述するラベルを使用して条件分岐をしてそれぞれ処理を書いていきます。
switch文
COPY
switch
(
初期化文(opt) 条件 )
文
caseラベル
COPY
case
整数定数式 :
defaultラベル
COPY
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文でいいことにします。
コード
コード
COPY
#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" ;
}
}
}
}
}
制約
COPY
Nはintに収まる整数の値 Aは"y"もしくは"n" (ただし、0<=N<=54である場合のみ入力されます)
出力
COPY
以下の表に従って出力してください。
点数と出力の関係(中学生)
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
COPY
$ -1 受けていないですね?
実行結果例4
COPY
$ 70 よくがんばりました。
実行結果例5
COPY
$ 60 もうすこしがんばりましょう。
実行結果例6
COPY
$ 50 y ぎりぎりですね。がんばりましょう。
実行結果例7
COPY
$ 50 n もうすこしがんばりましょう。
実行結果例8
COPY
$ 40 y もっとがんばりましょう。
実行結果例9
COPY
$ 40 n ぎりぎりですね。がんばりましょう。
実行結果例10
COPY
$ 30 n もっとがんばりましょう。
ヒント1
評価で分類しているので、switch文にするには、粗点を+ 5
してから/ 10
するのがいいでしょう。 そうすれば、例えば(95 + 5) / 10
で10
になり、(94 + 5) / 10
で9
になりますね。
ヒント2
複数の評価で同じ動作をする場合は、フォールスルーを使うといいでしょう。
回答例
回答例
COPY
#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
(C) 2020 駒場東邦物理部[KTPC]