暗黙変換の結果によって生まれた一時オブジェクトに気をつけて

次に示される関数 implicit_cast について考える.

#include <type_traits>
#include <utility>
 
template<class To, class From,
  typename std::enable_if<
    std::is_convertible<From, To>::value
  >::type* = nullptr
>
To implicit_cast(From && x) {
  return std::forward<From>(x);
}

この関数は,引数として渡された値を,型 To に暗黙変換して返すものであり,

例えば

implicit_cast<double>(0);

のように使う.


さて,この関数 implicit_cast には問題点がある.
このような例を考えよう:

auto x = 0;
auto y = implicit_cast<double const&>(x);

このとき, x の型は int であるため,インスタンス化された implicit_cast の実装は

double const& implicit_cast(int& x) {
  return std::forward<int&>(x);
}

となるが,これは

double const& implicit_cast(int& x) {
  double const& ref = x;
  return ref;
}

であり,((std::forward は lvalue reference に対しては特に何もしない))

double const& implicit_cast(int& x) {
  double temp = x;
  double const& ref = temp;
  return ref;
}

と同じ動作をする.


なぜなら, x の型は int である一方,関数の戻り値の型は double であり,
この二つの参照は互換性がないからだ.


見て分かる通り,この関数は,ローカル変数に対する参照を返している.
この動作は undefined behavior である. 動くかもしれないし,動かないかもしれない.

auto x = 0;
auto y = implicit_cast<double const&>(x);
std::cout << y << std::endl;  // 0 が出力されるとは限らない,もっと言うと ここが正しく実行されるかも分からない

よって,このような implicit_cast の定義は,決して書いてはいけない.


正しい implicit_cast の実装は,例えば以下のようになる:

template<class To,
  typename std::enable_if<
    std::is_convertible<To&&, To>::value
  >::type* = nullptr
>
To implicit_cast( typename std::enable_if<true, To>::type x ) {
  return std::forward<To>(x);
}

typename std::enable_if::type は,引数からの型推論を避けるために必要となる.
本当は typename std::identity::type と書ければ良いのだが, C++11 標準には identity は存在しないため,このような表記が必要となる.((std::common_type を使う流儀もあったが, LWG 2141 により もはや その流儀は使えなくなった. 詳しくは http://cpplover.blogspot.jp/2013/03/commontypedecay.html を参照.))


このような実装を採用すれば,関数内の一時オブジェクトが原因でコードが壊れることもなくなるし,

implicit_cast<void*>(0)

と書くことが可能になる*1という利点もある.


従って, implicit_cast 的なものを実装する場合には,基本的には こちらの実装を採用するべきである.


ただし,この場合だと, To がクラスの場合に無駄なコピーが作られてしまうので,
効率を考えれば,多少長くなっても

template<class To, class From,
  typename std::enable_if<
    !std::is_reference<To>::value &&
    std::is_convertible<From, To>::value
  >::type* = nullptr
>
To implicit_cast(From && x) {
  return std::forward<From>(x);
}

template<class To,
  typename std::enable_if<
    std::is_convertible<To&&, To>::value
  >::type* = nullptr
>
To implicit_cast( typename std::enable_if<true, To>::type&& x ) {
  return std::forward<To>(x);
}

と実装するのが妥当だろう.


また,この implicit_castconstexprnoexcept に対応していない,
といった事を考えると,実際の実装は もう少し複雑になるが,その辺りの説明は今回は省略したい.


* * * * *


さて,このような実装を採用すると,関数内で一時オブジェクトが作られることはなくなるが,
関数の外では

auto x = 0;
auto y = implicit_cast<double const&>(x);

auto x = 0;
auto y = implicit_cast<double const&>( double(x) );

と書かれてるのと同じになったため, auto を使わず

auto x = 0;
auto const& y = implicit_cast<double const&>( double(x) );

と書いた場合には, y は一時オブジェクトへの参照を保持することになり,上手くいかない.


この寿命問題を解決するには,そもそも implicit_cast を使うのを諦めて

int x;
double const& y = x;

と書けば良い*2が,その場合,

auto x = 0;
double const& y = x;
x = 23;
assert( y == 23 );  // ASSERTION FAILED!

と書いた場合に,一見して yx と同じオブジェクトを指しているように見えるが,
実際には別のオブジェクトであるため, x の変更は yに反映されず,
予想外の動作となる場合が生じる.


このような「暗黙変換の結果として作られた一時オブジェクトが,参照に束縛される」ケースは,
そのような挙動になることが一見して分かりにくいため,あまり好ましい動作とは言えない.


が,この挙動がない場合には,

void do_something( std::string const& x );
do_something("hoge");

のようなコードがコンパイルエラーになってしまうため,利便性のためには必要である.


see also: http://d.hatena.ne.jp/gintenlabo/20110309/1299688709


個人的には,関数の引数の場合のみを特別扱いにして,それ以外の場合には
暗黙変換された結果として生じた一時オブジェクトを参照に束縛することは エラーにするべきだと考えている.
どうしても一時オブジェクトを参照へ束縛したい場合には,明示的にコンストラクタを呼べばいい.



なお,先ほどの問題に対処したい場合には, std::reference_wrapper を使って,

auto x = 0;
std::reference_wrapper<double const> y = x;  // error, because x is not double

と書くと良いだろう.

std::reference_wrapper 使うべし. イヤーッ!

追記 (25:44, 4/18)

このような暗黙の一時オブジェクト生成を防ぐために attribute を使うという方法を思いついた.

double const& implicit_cast(int const& x) {
  [[avoid_reference_to_temporary]] return x; // compile error!
}

的な.

このネタに対しては他にも書きたいことは多いし,寝て起きた後に掘り下げてみようと思う.

*1:最初の実装だと int から void* へ変換しようとして失敗する

*2:一時オブジェクトが参照変数に直接 束縛された場合,参照先のオブジェクトの寿命は,参照変数の寿命に合わせて延長される