型推論
変数を宣言する場合はint型やdouble型などの具体的なデータ型名が必要です。
C++11から、初期化子によってデータ型が推測できる場合は記述を簡略化できる機能が追加されています。
これを型推論といいます。
auto指定子
#include <iostream>
int main()
{
int numberA = 10;
double realA = 20.0;
//型推論
auto numberB = 30;
auto realB = 40.0;
std::cout << typeid(numberA).name() << std::endl
<< typeid(realA).name() << std::endl
<< typeid(numberB).name() << std::endl
<< typeid(realB).name() << std::endl;
std::cin.get();
}
int double int double
数値リテラルはデータ型が決まっているので、その値からそのデータ型を特定することができます。
このような場合は、データ型名の代わりにauto
というキーワードを指定することで、自動的に適切なデータ型の変数を作ることができます。
typeid
は指定した値のデータ型の情報を格納するstd::type_info
というクラスを取得する演算子です。
このクラスのname
メンバ関数は、データ型名を返します。
typeid().name
関数で得られる型の名前は実装に依存します。
例えば同じint型でもコンパイラによっては「int」だったり「i」だったりします。
上のようなコードではauto
ではなく従来通りint
やdouble
と書いても同じことで、メリットはほぼありませんが、データ型名が長くなる場合などは記述が簡潔になりコードが読みやすくなります。
たとえばイテレータはデータ型名が長くなりがちなので、auto
を使うことですっきりと記述できるようになります。
std::vector<int> vec{ 0, 1, 2, 3, 4 };
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it)
{
std::cout << *it << std::endl;
}
//↓auto使用
for (auto it = vec.begin(); it != vec.end(); ++it)
{
std::cout << *it << std::endl;
}
その他、上記のような場合はvectorクラスから別のコンテナクラス(例えばlistクラス)にデータ型を変更した場合でもイテレータの宣言の箇所は修正しなくても良いというメリットもあります。
型が推測できない場合にはauto
は使用できません。
例えば初期化子がない場合などはエラーになります。
auto
というキーワードは、C++03までは自動変数を表すためのものでした。
自動変数とは非静的なローカル変数のことで、データ型の前にauto
を付けることでstatic変数でないことを示すことができました。
しかしローカル変数はstatic
を付けなければauto
を付けたことになるため省略可能で、使用されることがほとんどないキーワードだったため規格から削除され、全く異なる機能が割り当てられました。
C++03までのauto
は「自動記憶域期間指定子」、C++11からのauto
は「autoプレースホルダ型指定子」と言います。
const修飾、ポインタ、参照との併用
auto
はconst
やポインタ、参照と同時に指定することができます。
int a = 1;
const auto b = a;
auto* c = &a;
auto& d = a;
const auto* e = &a;
const auto& f = a;
型推論のルール
auto
による型推論は以下のような特徴、制限があります。
これはテンプレート引数のデータ型の推論でも同様です。
配列はポインタと推論される
配列型はポインタ型に型推論されます。
配列のサイズ情報が失われるので注意が必要です。
ただし参照で受け取る場合は配列のまま型推論できます。
int arr[] = { 1, 2, 3 };
auto auto_arr1 = arr; //int*型
auto& auto_arr2 = arr; //int[3]&型
なお、配列の初期化に使用する初期化子リストを直接型推論した場合はstd::initializer_list型となります。
これはauto
による型推論だけの例外で、型テンプレート引数では初期化子リストは型推論できません。
constや参照の削除
auto
による型推論は、対象となるデータ型が参照である場合や、const
、volatile
修飾子が付けられている場合、それらを無視します。
(constとvolatileを合わせてcv修飾と呼びます)
const int a = 1;
//constが外れる
//ただのint型
auto auto_a = a;
auto_a = 2; //書き換え可能
int b = 1;
int& ref_b = b;
//参照ではなくなる
//ただのint型
auto auto_b = ref_b;
auto_b = 2;
std::cout << b << std::endl;
std::cout << auto_b << std::endl;
//1
//2
ただし、受け取り側がポインタまたは参照の場合はconstはそのまま保持されます。
参照は外れるので、「参照のポインタ」や「参照の参照」になることはありません。
int a = 1;
const int& cr_a = a;
//int型(constが外れる)
auto auto_a1 = cr_a;
//const int*型
auto* auto_a2 = &cr_a;
//const int&型
auto& auto_a3 = cr_a;
なお、削除されるのは変数自体を修飾しているconstやvolatileです。
例えばconst char* str;
の場合、constは変数str
の書き換え禁止ではなくポインタの先の値の書き換え禁止を意味しています。
こういったconstは削除されません。
const char* const str;
ならば、変数strの書き換えを禁止している二番目のconstが削除されます。
右辺値参照変数は左辺値参照になる
auto
は右辺値参照型変数を右辺値参照型として型推論することはできません。
これはC++の値カテゴリの性質によるもので、右辺値参照型の変数それ自体は左辺値であるためです。
auto&&
と右辺値参照型で宣言しても左辺値参照型になります。
int num = 0;
int& a = num; //int&型
auto auto_a1 = a; //int型
auto& auto_a2 = a; //int&型
auto&& auto_a3 = a; //int&型
int&& b = 1; //int&&型
auto auto_b1 = b; //int型
auto& auto_b2 = b; //int&型
auto&& auto_b3 = b; //int&型
//これはint&&型
auto&& auto_rvalue = 2;
右辺値参照型変数が参照する元のデータに対する型推論はdecltypeで可能です。
値カテゴリについてはムーブセマンティクスの項を参照してください。
関数の戻り値の型推論
C++14以降、関数の戻り値の型にもauto
を指定することができます。
auto f1() { } //void型
auto f2() { return; } //void型
auto f3() { return 1 + 2; } //int型
戻り値の型はreturn文から型推論されます。
戻り値を指定しない場合はvoid型となります。
return文が複数ある場合、すべてのreturn文で戻り値の型は同じでなければなりません。
(cv修飾と参照は無視されます)
//int型(cv修飾は外れる)
auto f1(bool x)
{
int a = 0;
volatile const int b = 1;
if (x)
return a;
else
return b;
}
//int型(参照は外れる)
auto f2(bool x, int& y)
{
if (x)
return 1;
else
return y;
}
//returnに異なる型の指定
//コンパイルエラー
auto f3(bool x)
{
if (x)
return 0;
else
return 1.0;
}
初期化子リストはreturn文で型推論することはできません。
クラスのメンバ関数の戻り値にもauto
は使用できますが、仮想関数には使用できません。
auto f()
{
//NG
//return { 1, 2 };
}
class C
{
//OK
auto f1() { return 1; }
//NG
//virtual auto f2() { return 2; }
};
関数ポインタ
関数ポインタの変数宣言はauto
で簡単に書くことができます。
#include <iostream>
int f1(int a, int b) { return a + b; }
int f2(int a, int b) { return a * b; }
int main()
{
auto f = f1;
std::cout << f(2, 3) << std::endl;
//関数ポインタをf2に差し替え
f = f2;
std::cout << f(2, 3) << std::endl;
//従来の関数ポインタの書き方
//戻り値がint型、引数にint型ふたつの関数ポインタgの宣言
int (*g)(int, int) = f1;
}
5 6
decltype指定子
データ型名は通常はソースコード上の必要な箇所にそのまま記述されますが、decltype
指定子を使用すると、式からデータ型を取得することができます。
取得できるのは単なる文字列ではなく、そのデータ型をソースコード上に記述したのと同じ効果を得ることができます。
つまり、取得したデータ型の変数を作ることができます。
(C++11以降で使用可能です)
int func(int, int) { return 0; }
int main()
{
int a = 0;
//int型変数の定義
//int b = 1;と同じ
decltype(a) b = 1;
decltype(0) c = 2; //int型
decltype(.0) d = 3.0; //double型
int arr[] = { 1, 2, 3 };
//int[3]型
decltype(arr) e = { 4, 5, 6 };
//int型
decltype(func(0, 0)) f = 4;
//関数ポインタ
decltype(&func) g = func;
}
decltype
に渡された式はコンパイル時にデータ型を決定するために使用されますが、式そのものは実行されません。
関数呼び出し式を指定した場合でもその関数は実行されないので、実装がない関数でもデータ型は取得できます。
ただし関数ポインタの取得は関数の実体が必要なので、上記コードの21行目は(定義がなければ)関数のアドレス取得時にエラーになります。
関数がオーバーロードされていて、関数名だけでは対象があいまいになる場合はエラーになります。
波括弧初期化子リストはデータ型を持たないため、decltype
では型を取得することはできません。
auto
の場合はstd::initializer_list
型に変換されます。
//エラー
//decltype({1}) a = {1};
//decltype({ 1, 2 }) b = { 1, 2 };
//std::initializer_list<int>型
auto c = { 1, 2 };
decltype
はイテレータなどの型名を簡便に記述するのにもよく用いられます。
std::vector<int> vec;
//同じ意味
std::vector<int>::iterator it1;
decltype(vec)::iterator it2;
型取得のルール
decltype
に変数等の名前(識別子)をそのまま記述した場合と、それ以外の式(1+1とか)を指定した場合とでは動作が異なります。
int arr[] = { 1, 2, 3 };
//int[3]型
decltype(arr) a;
//int*型
decltype(&arr[0]) b;
int num = 0;
//int型
decltype(num) c;
//int&型
decltype((num)) d = num;
変数等の名前を単体で指定した場合は、その実体のデータ型が使用されます。
その名前に何らかの演算子を加えた場合は以下のルールに従ってデータ型が決定されます。
- 式が右辺値参照(xvalue)の場合はそのデータ型の右辺値参照型
- 式が左辺値(lvalue)の場合はそのデータ型の左辺値参照型
- それ以外の場合(prvalue)は式が表すデータ型
配列名のarr
と&arr[0]
は通常はどちらも先頭要素へのポインタですが、後者は変数名そのままではなく演算子つきの式なので、decltype
では上記のルールに従い異なるデータ型が取得されます。
変数名を丸括弧で囲った場合、他の場所では動作は変わりませんがdecltype
に渡した場合は「名前そのまま」のルールに当てはまらなくなります。
丸括弧を含めた式は左辺値なので、decltype
はその参照型を取得します。
左辺値や右辺値参照などについてはムーブセマンティクスを参照してください。
関数名をそのまま記述した場合は関数のプロトタイプ宣言のデータ型が取得されます。
これはテンプレート等で関数を渡す際のデータ型の指定などに利用できます。
#include <iostream>
void func(int a) {
std::cout << a << std::endl;
}
template<typename T>
void funcT()
{
//関数ポインタ
T* a = func;
a(1);
}
int main()
{
funcT<decltype(func)>();
}
decltype(auto)
decltype
にauto
を指定すると、データ型を右辺の式から推論して決定します。
これはC++14以降で可能です。
int a = 0, b = 1;
decltype(a + b) c = a + b; //int型
decltype(auto) d = a + b; //int型
decltype(auto)
を使用しない場合、a + b
の結果を格納する適切なデータ型を得るために、同じ式を二度書く必要があります。
decltype(auto)
は右辺の式をdecltype
に適用したデータ型を得ることができ、記述が簡潔になります。
型の決定ルールはdecltype
のものになります。
つまりauto
とは違いcv修飾や参照は外れません。
また、cv修飾や参照を新たに加えることはできません。
関数の戻り値の型指定にも使用できます。
//参照を返す関数
decltype(auto) f(int& a)
{
return a;
}
decltype(auto)
は上記のように関数の戻り値として参照をそのまま返す場合や、関数の後置戻り値型で使用されます。