C++11 型推論(auto, decltype)の徹底活用
C++11, 14 により C++ でも 型推論(auto) が使えるようになりました。
C++11 の機能の中でも型変換は手っ取り早くコードを書くのが楽になるので、
使っている人は多いと思います。
今回はその型推論をもっと徹底的に使っていこうというお話です。
型推論とは
C++ のような静的型付けの言語では変数の型はきっちり指定しておく必要があります。 いちいち書くのはめんどくさいですが、 コードの安全性や実行プログラムの速度に効果があります。一方、Ruby や Python のような動的型付け言語では、 変数自体が型情報を持っていて、型を指定する必要がなく楽ちんですし、 配列に任意の型を入れられるといった柔軟性があります。
これはどちらが優れているというものではなく、言語には適材適所があるということです。
とはいえ、めんどくさいものはめんどくさいので、「少しでも楽をしよう」というのが型推論です。
型推論では変数の型を与えられた初期値から推論します。 それを使って、型を書くのを省略できるところは省略してしまおうという方法です。 C++ の型推論では型宣言に auto と書きます。 それをコンパイラーが初期値から推論して型を決めてくれます。
型推論はどこまで使えるのか
型推論はローカル変数以外にも使えます。 対象をまとめると次のようになります。対象 | 対応 |
---|---|
ローカル変数 | ○ |
グローバル変数 | × |
クラスのメンバー変数 | × |
クラス変数(static) | const のみ可 |
関数の戻り値 | 仮想(virtual)関数以外 (C++14) |
関数の引数 | ラムダ式のみ可 (C++14) |
C++14 は C++11 の補足的な変更なのですが、C++14 で結構広がりました。 特に「関数の戻り値」の型推論は意外な実力を秘めています。
それを含め次章から個々の型推論について説明してきます。
ローカル変数
通常ローカル変数
関数や制御文のブロック内での変数で、型推論として一番よく使う場面だと思います。int foofunc() { auto localver = 1;const やポインター(*)、参照(&)等に変えたい場合、修飾子は別途付ける必要があります。 (もともとポインターであれば auto でポインター)
リテラルの工夫
文字列リテラルを初期値として渡す場合は型推論では const char * になります。(環境によって char * かも)しかし std::string にしたいという時もあると思います。 そういった場合には C++14 で追加された basic_string のリテラルを使うと出来ます。
auto cstr = "Hellow"; // const char * auto str = "Hellow"s; // std::stringそのほかのクラスでよく使うものがあれば、C++11 では自作のリテラルを定義することもできるようになっています。
範囲 for
明示的に初期値を与えているわけではありませんが、C++11 の 範囲 for 文 でも型推論は使えます。vector<string> strs = { "foo", "bar", "baz" }; for (const auto &elem : strs) { cout << elem << endl; }
グローバル変数
グローバル変数の場合には extern 宣言を必要とするため、使うことはできません。extern int64_t ErrNo;ファイルローカル、逆に言うとファイル内でグローバルな static 変数に関しても、auto が使えます。
static auto stvar = 1.0;static を外したものは分類としては名前空間スコープ変数に入り、auto は使えます。 そのままだと普通は使いませんが、const を付けて定数として公開することはあります。
const auto VERION = "1.0.0";
クラス関連の変数
クラス変数 (静的メンバー変数)
クラス定義で static を付けると静的メンバーとなり、変数はクラス変数となります。 クラス変数は const の場合、初期値を指定することができます。class Foo { static const int Dim = 2; // static int Dim = 2; NG const のみ OK static constexpr double Mag = 0.5;const が付けられるのは int のみです。C++11 では constexpr を付けると他の型でも初期値が指定できるようになっています。 const ではないクラス変数では初期値を指定できないので auto は使えませんが、 上記のようなクラス定数であれば auto が使えます。
class Foo { static const auto Dim = 2; static constexpr auto Mag = 0.5;
インスタンス変数 (メンバー変数)
メンバー変数でいえば、C++11 からクラス変数ではない通常のメンバー変数(インスタンス変数)でも初期値を指定できるようになりました。 初期値を指定できるので auto も使えるのではないかなと思ったのですが、auto の対象外っぽいです。class Foo { /// メンバー変数 int m_bar = 0; // auto m_baz = 0; NG
関数の戻り値
戻り値の型推論
C++14 から戻り値の型推論もできるようになりました。例えば次の関数では戻り値は a または b でともに T なので、 コンパイラーが T と確定します。
template <typename T> auto StdMax(const T &a, const T &b) { return (a < b) ? b : a; }
cout << StdMax(4, 12) << endl; // 12戻り値の型推論は通常関数、クラスのメンバー関数にかかわらず使えます。 ただし、 virtual なメンバー関数の場合は auto は使えません。 これは、オーバーライドのチェックと仮想関数テーブルのレイアウトが複雑になるためらしいです。
型が確定しない戻り値 : 三項演算子
前節の StdMax() は std::max() とほぼ同じなのですが、 整数のリテラルと int64_t や double を比較するとコンパイルエラーになるので、使いづらいです。 そこで、引数の型を別々にできるようにしてみます。template <typename T, typename U> auto MyMax(const T &a, const U &b) { return (a < b) ? b : a; }こうすると、引数によって戻り値の型がかわる関数を作成することができます。
cout << MyMax(4.2, 12) << endl; // 12 cout << MyMax(4.2, 2) << endl; // 4.2
型が確定しない戻り値 : 複数の return 文
複数のリターン文で戻り値の型が確定しない場合には型推論はできません。 前節の関数を if 文を使うように直してみます。template <typename T, typename U> auto MyMax2Return(const T &a, const U &b) { if (a < b) { return b; } else { return a; } }これだと
MyMax2Return(4.2, 12)
のように T, U が別々の型になる場合にはコンパイルエラーとなってしまいます。
cout << MyMax2Return(4, 12) << endl; // 12 // cout << MyMax2Return(4.2, 12) << endl; // コンパイルエラー「さっきはできたなのになんで」って思いますが、 どちらかというと 出来る方が C++11 の仕様的にいいのかな という感じです。
これに限って言えば三項演算子に戻せばいいのですが、 「どうしても複数の return 文する必要がある」として解決策を考えてみます。
どうすれば解決できるかというと、C++11 で追加された式の型を取得する decltype と後置の戻り値型宣言を使います。
template <typename T, typename U> auto FixedMyMax2Return(const T &a, const U &b) -> decltype(a+b) { if (a < b) { return b; } else { return a; } }"a+b" の型というのは、例えば double と int との変数を足すと double になります。 基本型の四則演算の結果のように新しい型を決めてその値を返します。
cout << FixedMyMax2Return(4.2, 12) << endl; // 12.0 cout << FixedMyMax2Return(4.2, 2) << endl; // 4.2
decltype の活用
decltype を使えば、今まで出来なかったようなことができるようになります。例えば、以下のような点クラス(struct)があったとします。
template <typename T> struct Point { T x; T y; };このクラスの掛け算(*)の演算子をオーバーロードしたとして、次の計算をします。
Point<int>(3,2) * 0.5戻り値をしては Point<double>(1.5,1.0) のような double の点を返してくれるのが一番自然だと思います。
しかし、今まではこれを実現するのは不可能でした。 正確には基本型の全種別を個別に定義していけば可能ですが、現実的ではありません。
これが decltype と戻り値の型推論を使えば、基本型のように型を変えるクラスを簡単に定義できます。
template <typename T, typename U> inline auto operator*(const Point<T> &a, U b) { return Point<decltype(a.x*b)>(a.x*b, a.y*b); }
cout << Point<int>(3, 2) * 0.5 << endl; // Point<double>(1.5, 1.0)ちなみに decltype と後置の宣言でも定義可能です。
template <typename T, typename U> inline auto operator*(const Point<T> &a, U b) -> Point<decltype(a.x*b)> { return {a.x*b, a.y*b}; }
関数の引数
引数宣言の auto
関数の引数の宣言に auto を使うことを考えてみます。先ほどの点クラスに引数、戻り値ともに auto で指定したとします。
template <typename T> struct Point { : auto Plus(auto val) const { return Point(static_cast<T>(x+val), static_cast<T>(y+val)); }これは初期値から推測する型推論ではなくなっているのに気付かれるでしょうか。 こうなってくるとテンプレートを簡略化した 自動ジェネリック というものになってきます。
ただ、残念ながらこれはコンパイルできるようになっていません。 しかし、仕様上は OK なのかはわかりませんが、 戻り値の型推論をしないと gcc ではコンパイルが通ります。
// 引数だけ auto Point PlusAutoArg(auto val) const { return Point(static_cast<T>(x+val), static_cast<T>(y+val)); }前章と同様に戻り値だけ auto も出来ます。
// 戻り値だけ auto template <typename U> auto PlusAutoReturn(U val) const { return Point<decltype(x+val)>(x+val, y+val); }
ジェネリックラムダ (C++14)
C++14 からラムダ式に限り、 引数、戻り値ともに auto を指定できるようになっています。auto plus = [](auto a, auto b) { return a + b; }; cout << plus(2, 3) << endl; // result = 5 cout << plus(2.5, 3) << endl; // result = 5.5これまた、「これが大丈夫なのに、さっきのはなんでダメなの」という気はします。 ただ、ラムダ式のように関数内に書くのに template 宣言は書けないですし、 頑張って仕様として入れたのではないかと思います。
サンプルコード
今まで出てきたコードのファイルもあげておきます。- 関連記事
-
- ブログ始めました
- ブログをリニューアルしました
- 最近ヒッグス粒子が話題になっているので、量子に興味が出そうなお勧め本
- C++11 型推論(auto, decltype)の徹底活用
Facebook コメント
コメント