http://d.hatena.ne.jp/gintenlabo/20110220/1298187803
の解答と解説です。
参考規格: 7.1.6.4 auto specifier [dcl.spec.auto] , N3225
解答および簡単な解説
int f(); int& g(); int&& h(); int main() { int x0 = 0; // リテラル 0 は int に推論される int i = 1; int x1 = i; // i は int 型の lvalue int const j = 2; int x2 = j; // j は int const 型の lvalue int a[3]; int* x3 = a; // a は int [3] 型の lvalue int const b[] = { 1, 2, 3, 4 }; int const* x4 = b; // b は int const [4] 型の lvalue int x5 = f(); // f() は int 型の prvalue int x6 = g(); // g() は int 型の lvalue int x7 = h(); // h() は int 型の xvalue int (*x8)() = f; // f は int () 型の lvalue std::initializer_list<int> x9 = { 1, 2, 3 }; // { 1, 2, 3 } は braced-init-list }
N3225 の 7.1.6.4 auto specifier [dcl.spec.auto] より、
auto x = initializer;
という形で定義される変数 x
の型は、
template<class T> void f( T x );
という関数 f
に対し、 f( initializer );
という形で関数呼び出しを行った場合に
T が推論される型になる。
ただし initializer が braced-init-list ( { 1, 2, 3 }
のような形)ならば、
initializer は std::initializer_list<U>
型として扱われる。
よって、 x0, x5
はそのまま int
型に( 0
は文脈によって NULL ポインタや他の算術型になる可能性があるが、テンプレートによって型推論される場合には int
として扱われる)、
x1, x2, x6, x7
は lvalue-to-rvalue conversion により同じく int
型に、
x3, x4
は array-to-pointer conversion によりそれぞれ int*, int const*
型に、
x8
は function-to-pointer conversion により int (*)()
型に、
x9
は各要素が int
型の braced-init-list なので std::initializer_list
型に推論される。
別解
auto x = initializer;
という形の変数定義において、 x
の型は、 initializer が braced-init-list でない場合には
typename std::decay<decltype( (initializer) )>::type
で得ることができる。
ここで、
decltype( (0) )
はint
、故にx0
の型はstd::decay
つまり::type int
decltype( (i) )
はint&
、故にx1
の型はstd::decay
つまり::type int
decltype( (j) )
はint const&
、故にx2
の型はstd::decay
つまり::type int
decltype( (a) )
はint (&)[3]
、故にx3
の型はstd::decay
つまり::type int*
decltype( (b) )
はint const(&)[4]
、故にx4
の型はstd::decay
つまり::type int const*
decltype( (f()) )
はint
、故にx5
の型はstd::decay
e つまり::typ int
decltype( (g()) )
はint&
、故にx6
の型はstd::decay
つまり::type int
decltype( (h()) )
はint&&
、故にx7
の型はstd::decay
つまり::type int
decltype( (f) )
はint (&)()
、故にx8
の型はstd::decay
つまり::type int (*)()
{ 1, 2, 3 }
は各要素がint
型の braced-init-list なのでx9
の型はstd::initializer_list
これを元に auto を書き換えると、上記の解答に一致する。
詳細な解説
まず一番最初に理解しておくべきなのは、
auto x = initializer;
という形で変数 x
を定義した場合、 x
は原則的に「 initializer のコピーになる」ということです:
#include <iostream> #include <vector> int main() { int const n = 5; auto x = n; // x はコピーされるので、 const は付かない ++x; // なので変更できる auto const c = n; // const にしたい場合には、明示的に const を付ける // ++c; // c は const なので変更できない std::vector<int> v(c); v.front() = 0; // std::vector の front() は、先頭要素への参照を返す std::cout << v.front() << std::endl; // 0 auto y = v.front(); // が、 auto によって変数に束縛された場合、コピーされるので、 y = 1; // 束縛先の変数の値を変更しても、束縛元には関係ない std::cout << v.front() << std::endl; // 0 // 束縛元の変数を変更したい場合には、 auto& を用いて「参照である」旨を明示する auto& ref = v.front(); ref = 1; std::cout << v.front() << std::endl; // 1 }
言い換えるなら、
auto x = initializer;
という形で定義された x
は、 initializer が参照だったり const
/volatile
で修飾されていた場合には、
それらの修飾が全て取り除かれた型の変数になります。((勿論、式
の型がポインタや std::reference_wrapper
等の「参照を値として保持する」型の場合には、深いコピーではなく浅いコピーが行われ、また参照先への const
/volatile
修飾はそのまま残ります。))
これはつまり、例え initializer が参照であった場合でも、 x
は initializer の参照するオブジェクトとは独立した変数となる、ということで、
こうすることにより、参照を返す関数の戻り値をうっかり auto で受けた結果、参照先を変更する、といったミスを防ぐことが出来るようになっています。
ただし、「原則的に」という表現を用いたように、これには幾つか例外があります。
まず些細な例として、束縛元の式の型が
int f(); auto x8 = f;
のような関数(この例では、関数型 int()
の lvalue )の場合には、
そもそも関数というのはオブジェクトではないので、コピーして変数に束縛することが出来ません。
よって、そのような場合には、渡された関数(への参照)から関数ポインタへの暗黙変換が行われ、
int (*x8)() = f;
のように定義されます。(( int (*)() x8 = f;
と間違えた人、そう書きたくなる気持ちは痛いほど分かりますが、 C++ の文法的にはアウトなので、その辺りの文法を確認しなおすことを勧めます。))
これは極めて真っ当な動作で、そう言われてみなければ気づかないレベルのものです。
問題は、 initializer の型が
int a[3]; auto x3 = a;
のような配列(この例では、配列型 int [3]
の lvalue )の場合で、その場合、
配列の中身はコピーされず、先頭要素を指すポインタへと暗黙変換されて
int* x3 = a;
のように束縛されます。((なお、 std::array
や std::vector
等のクラスは配列ではないので、きちんとコピーされます。))
これの何が問題なのかというと、この場合、束縛元の変数はコピーされないため、
int a[] = { 1, 2, 3 }; auto x = a; // 配列の場合、ポインタに変換され、中身はコピーされないので x[0] = 0; // auto によって束縛された変数を介して、束縛元を変更できてしまう std::cout << a[0] << std::endl; // 0 auto const y = a; // なお悪いことに、 const を付けても y[1] = 0; // 束縛元の配列を変更できてしまう std::cout << a[1] << std::endl; // 0 // この動作を避けるには、 // auto const& z = a; // z[2] = 0; // コンパイルエラー // または // auto const* z = a; // z[2] = 0; // コンパイルエラー // のように書く必要がある
上記のコードに示すように、うっかり配列内部を書き換えてしまう問題を引き起こす可能性があるのです。
じゃあ何故、配列に限ってはコピーが行われずポインタに変換されるか、というと、この記事の冒頭にも書いたように、
auto x = initializer;
という表現によって推論される変数 x の型は、文法を単純化するため、
initializer が { }
で包まれたリストの形式でない限り、
template<class T> void f( T x );
という関数 f
を呼び出す際に行われる型推論と同じプロセスで決定されるからです。
なお、これは auto const& や auto* という書き方をした場合でも同じで、順に
template<class U> void g( U const& y ); template<class V> void h( V* z );
という関数テンプレートによって推論される y, z と同じように初期化されます。
また、変数 x が
auto x = { initializer-list };
のように { }
で包まれたリストの形で定義されていた場合、
{ initializer-list }
は、一旦 std::initializer_list
に推論された上で、
後は関数テンプレートの型推論と同じプロセスで決定されます(ここでは auto 単独なので、そのまま std::initializer_list
になります)。
なお、関数テンプレート template
に対し、直接
f( {1, 2, 3} );
のように braced-init-list を渡した場合、
コンパイラは型 T
を推論することが出来ません。(( gcc では関数テンプレートに直接渡した場合でも std::initializer_list
に推論されるのですが、現行の規格ではこのような動作は行われないことになっています(それを受け、 gcc でもコンパイラオプションによって禁止することができるようになっています)。))
C++ の規格では、
template<class T> void f( T x );
という関数テンプレートに対し、実引数として式 expr を与えた場合の型推論は、以下のように行われます:
- expr の型を
U
と置く。ただし expr が参照の場合には、U
はその参照を取り除いた型とする U
が配列の場合、T
はU
を array-to-pointer conversion によりポインタに変換した型とするU
が関数の場合、T
はU
を function-to-pointer conversion により、ポインタに変換した型とするU
が関数でも配列でもない場合には、T
はU
のconst
/volatile
修飾を取り除いた型とする
比較的複雑な型変換ですが、この型変換は標準ライブラリの std::decay
で行う事ができ、
T の型を得るには、 decltype と組み合わせて
typename std::decay<decltype((expr))>::type
とすることで得ることができます。
ここで注目すべきは、配列(や関数)が与えられた場合には、それらはポインタに変換される、ということで、
何故かというと、 C++ では、 C との互換性という名の歴史的経緯により、
void f( int x[N] );
という関数 f の宣言は、
void f( int* x );
という関数 f の宣言と全く同じ物であるとして扱われる以上、
テンプレートの型推論においても、その辺りに対する整合性を取る必要があるからで、
このような複雑な事情により、単独の auto を使った型推論では、
配列を渡した場合にはポインタに変換されるようになっているのです。((なお、 auto &
や auto const&
を使った場合には、配列や関数を渡した場合でも、ちゃんと「配列や関数に対する参照」として扱ってくれます。))
このように、 C++0x の言語仕様は、 auto 一つとっても非常にややこしい奥の深いものとなっています。
勿論、実際に auto を使う場合には、こういうルールは殆ど気にする必要は無いわけですが、
興味が湧いたのならば、いろいろと調べてみると楽しいんじゃないでしょうか。