いい加減 僕は T t(x); と auto t = T(x); の違いを blog にまとめるべきかもしれない

http://cpplover.blogspot.jp/2013/04/isexplicitlyconvertible.html のコメント書いてて思ったので,書く.


まず些細な違いとして, auto は右辺の値の const や参照を消し飛ばす*1,という点が挙げられる.

using T = int const;  // Alias declarations;  I prefer this to typedef
double x = 1.2;

T t(x); // decltype(t) is int const
auto t = T(x); // const is removed; decltype(t) is int
using T = int const&;
int x = 42;

T t(x);  // t is a const reference to x; decltype(t) is int const&
auto t = T(x); // x is copied; decltype(t) is int

が,これは今回の記事では これ以上は言及しない.

詳しく知りたい方は http://d.hatena.ne.jp/gintenlabo/20110223/1298482597 を参照.


真に問題なのは, T がポインタの場合である:

using T = int*;
double d;
auto x = &d;

T t1(x);  // ill-formed
auto t2 = T(x); // well-formed(!), because T(x) is C-style cast

上記のコードで, auto t2 = T(x);コンパイルエラーとはならない.
なぜなら T(x) という式は, (T)x 同じ C-style cast だからだ.
そして,少しでも C++ を知っている人であれば あえて言うまでもない事実だが, C-style cast は邪悪である.
故に, T(x) という表現もまた,邪悪である.
良心ある C++ 使いを自覚するなら,決して使ってはならない.


* * * * *


さて,この挙動は, Variadic Templates を使う場合,特に問題になる.


例えば

template<class T, class... Args>
T create(Args&&... args) {
  return T( std::forward<Args>(args)... );
}

このような関数を作ったとき,

double d;
auto p = create<int*>(&d);

この式は問題なくコンパイルが通ってしまう上,多くのコンパイラでは警告すら出ない.


この挙動は,間違いなく危険である. よって,上記の create の定義は,面倒でも

template<class T, class... Args>
T create(Args&&... args) {
  T t( std::forward<Args>(args)... );
  return t;  // t is implicitly moved (or copy is omitted by NRVO)
}

と書くのが良い.((厳密に言うと T が const 修飾されてたり参照だったりする時には上手く行かないので,そのようなケースには別個 対応する必要がある))


もしくは, uniform initialization を使って

return T{ std::forward<Args>(args)... };

と書く手もある.


ただ,これは T が参照の場合には上手くいかないし,
{} による初期化は () による初期化とは微妙に挙動が違うので,厳密には同じ動作にはならないことは留意するべきである.
(が,細かいことを気にしないならば {} で統一するのは有力である. 少なくとも大怪我はしない.)


また,上記の create を SFINAE に対応させる場合,

template<class T, class... Args>
auto create(Args&&... args)
  -> decltype( T( std::forward<Args>(args)... ) );

と書くと問題になるので,じゃあ一体どうすればいいのか,と思うかもしれないが,*2


標準には まさにその T t( std::forward(args)... ); の可非を判定するために
std::is_constructible というメタ関数が存在しているため,

template<class T, class... Args,
  typename std::enable_if<
    std::is_constructible<T, Args...>::value
  >::type* = 0
>
T create(Args&&... args) {
  return T( std::forward<Args>(args)... );
}

と書けば良い.


副次的な利点として,こう書けば不正なケースは SFINAE で除外されるため,
上記のように 直接 T( std::forward(args)... ) を返すことで, constexpr にも対応できるようになる.

*1:厳密に言うと decay する

*2:変数定義は式ではないため,簡単には SFINAE に使えない