初期化式付きの制御構文
20世紀のC++プログラマ(筆者)が、今(21世紀)のC++を知ってあっと驚く連載の第7回です。今回は、前回に引き続きモダンな言語仕様を、C++ 14以降にフォーカスして紹介します。まずは、初期化式を持てるようになったif文とswitch文、そしてfor文です。
初期化式付きif文[C++17]
初期化式を持てるという意味は何でしょうか? これまでのif文では、何らかの変数を含む式を条件式とする場合、その変数はif文の外部で宣言されている必要がありました。例えば、以下のリストのようにです。
srand((unsigned)time(NULL)); auto x = rand(); if (x % 2 == 0) …
仮に変数xがif文終了後には用済みとなっても、変数xは引き続き有効になっています。もちろん、xは再宣言できませんが、別の用途で使うことができます。ただ、変数の用途が途中で変わるのはトラブルの元であり、あまり好ましいこととは言えません。
そこで、C++ 17においてはif文に初期化式を指定できるようになりました。for文のように、条件式の前に初期化式が来るという感じです。if文の内部でのみ有効としたい変数は、初期化式で宣言すればif文の終了とともに消滅します。以下のリストでは、変数xを乱数で初期化し、すぐに偶数か奇数か判定しています。if文が終了すれば、変数xは別の型で再宣言できます。
if(auto x = rand(); x % 2 == 0) {
cout << x << "は偶数です。" << endl;
} else {
cout << x << "は奇数です。" << endl;
}
// 実行結果:97872224は偶数です。
auto x = 1.0; // 別の型で再宣言可能
初期化式付きswitch文[C++17]
switch文も同様です。以下のリストでは、変数yを乱数で初期化し、2の剰余算で偶数、奇数を振り分けています。この場合も、switch文が終了すれば、変数yは別の型で再宣言できます。
switch(auto y = rand(); y % 2) {
case 0:
cout << y << "は偶数です。" << endl;
break;
case 1:
cout << y << "は奇数です。" << endl;
break;
}
// 実行結果:2113478813は奇数です。
auto y = 'y'; // 別の型で再宣言可能
初期化式付きfor文[C++20]
最後のfor文も同様です。「for文には最初から初期化式があるでしょ?」というツッコミが来そうですが、範囲for文すなわちコレクションに適用する方です。この範囲for文、確かに便利なのですが、例えばループ中で「今、何番目?」というのを把握することができませんでした。それをやりたい場合、カウンタ変数を用意し、ループ中でカウントアップさせるなどする必要がありました。ここでも、ループ中でしか必要ない変数がfor文終了後も残ってしまうという問題が起きます。以下のリストでは、配列の中身を走査して表示しますが、カウンタ変数countを用意して順番も表示しています。
auto ary = {"Mon", "Tue", "Wed", "Thu", "Fri"};
for(int count = 1; auto& a: ary) {
cout << count++ << ":" << a << endl;
}
// 1:Mon …
auto count = "OK"; // countを宣言可能
[NOTE]C++ 23では型指定にusingによるエイリアスも指定できる
初期化式中の型名には、typedefによる別名も指定できます。C++ 20まではusingによる別名は指定できませんでしたが、C++ 23ではこれが可能になりました。細かな改良ですが、使いやすさはエディションごとに進化しています。
タプルとバリアント
言語仕様というよりはライブラリの拡張ですが、タプルとバリアントを紹介します。構造体によらず複数の型をまとめて扱ったり、型が特定されない値を扱うのに便利です。多くのプログラミング言語で実装されている機能ですね。
タプルstd::tuple[C++11]
タプルは、複数の型の値の組を扱えるクラスです。配列では同じ型の値しか取り扱えませんが、タプルでは異なる型の値をまとめて関数の引数としたり、戻り値とすることができます。タプルは0個からの要素数に対応しますが、0個1個では意味がないですし、2個ではstd::pairがあるので、一般的には3個以上の場合に使います。
tuple<int, double, string> tuple1 { 1, 123.45, "Hello!" };
tuple<string, int, char> tuple2 { "World!", 1, 'X' };
vector<tuple<string, double, double>> vec = {
{ "Taro", 175.5, 70.0 },
{ "Hanako", 160.5, 50.0 },
};
タプルの各要素へのアクセスは、std::get関数を使います。
cout << get<0>(tuple1) << "," << get<1>(tuple1) << "," << get<2>(tuple1) << endl; // 実行結果:1,123.45,Hello!
バリアントstd::variant[C++17]
バリアントは、単一のオブジェクトで複数の型のデータを保持できるクラスです。そういう意味では共用体(union)に似ていますが、利用できる型を厳密に制限できるので、共用体より安全に利用することができます。以下のリストでは、int型、double型、string型の値を持てるバリアントvariant1を宣言しています。
variant<int, double, string> variant1; variant1 = 100; variant1 = 3.14; variant1 = "Hello, world!";
バリアントの各要素へのアクセスは、タプルと同様にstd::get関数を使います。
cout << get<int>(variant1) << endl; // 実行結果:1
バリアントに入っている型と取得する型が異なる場合、実行時に例外が発生します。
variant1 = "Hello, world!"; cout << get<int>(variant1) << endl; // libc++abi: terminating due to uncaught exception of type std::bad_variant_access: bad_variant_access
バリアントに入っている値の型は、indexメソッドで取得できます。宣言時の型パラメータの順番に相当する0からのインデックスで返されるので、switch文などで型による振り分けが可能です。ただし、インデックスに対応した関数オブジェクトを定義した構造体を使うVisitorパターンを使えるstd::visit関数により、switch文などを使用せずにシンプルに振り分けも可能です。
struct Visitor {
void operator()(int) {cout << "Integer" << endl;}
void operator()(double) {cout << "Double" << endl;}
void operator()(string) {cout << "String" << endl;}
};
variant1 = "Hello, world!";
visit(Visitor{}, variant1);
// 実行結果:String
