Restricted Implicit Cast

related to: http://d.hatena.ne.jp/gintenlabo/20130416/1366130964


以下のような C++ コードを考える:

// http://ideone.com/s3I4n6
#include <iostream>

int main() {
  auto x = 0;
  double const& ref = x;
  x = 23;
  std::cout << x << std::endl;
  std::cout << ref << std::endl;
}

このコードをコンパイルして実行すると,

23
0

と出力される.


何故なら,一見して x を参照しているように見える ref は,実のところ
int から double へ暗黙変換が行われた結果の一時オブジェクトを参照しているからだ.


このコード http://ideone.com/EWG7Xn を実行してみると分かりやすい:

#include <iostream>
 
int main() {
  auto x = 0;
  double const& ref = x;
  x = 23;
  std::cout << &x << std::endl;
  std::cout << &ref << std::endl;
}

表示された二つのアドレスは,違うものになっているはずだ.


もちろん,普通に C++ を使うならば,このコードは

#include <iostream>
 
int main() {
  auto x = 0;
  auto const& ref = x;
  // ...
}

のように, auto を使って書けば良い.


仮に参照の型をチェックしたいのであれば, この前の記事で定義した implicit_cast と非 const を使って

#include <type_traits>
#include <utility>

template<class To>
To implicit_cast( typename std::enable_if<true, To>::type x ) {
  return std::forward<To>(x);
}

#include <iostream>

int main() {
  auto x = 0;
  auto const& ref = implicit_cast<double&>(x);  // error, int& cannot be converted to double&
  // ...
}

と書くか, std::reference_wrapper を使って

#include <iostream>
#include <functional>

int main() {
  auto x = 0;
  std::reference_wrapper<double const> ref = x;  // error, std::reference_wrapper<T>::reference_wrapper(T && ) is deleted
  // ...
}

と書くか,あるいは static_assert を用いて

#include <iostream>
#include <type_traits>

int main() {
  auto x = 0;
  auto const& ref = x;
  static_assert( std::is_same<typename std::decay<decltype(ref)>::type, double>::value, "" ); // error!
  // ...
}

と書けばよい.


しかし,これらの方法は,いずれも問題がある.


まず単純に, std::reference_wrapperstatic_assert を使う方法は面倒だ.


そして implicit_cast を使う方法は,

int main() {
  auto const x = 0.0;
  auto const& ref = implicit_cast<double&>(x);  // error, double const& cannot be converted to double&
  // ...
}

このように const 修飾に弱い.((このケースだと元々のオブジェクトが const なのでコピーされても問題ないが,非 const オブジェクトに対する const 参照と,元々 const なオブジェクトに対する参照は,言語的には区別できない.))


そこで,変換対象の型として参照型が指定されたとき,一時オブジェクトへの参照を作らない
ように定義された implicit_cast があるといいな,という話になる.


というわけで,作ってみた.
変換先が関数型や void である場合に対処してなかったり, noexceptconstexpr にも対応してないなど
不足はあるが,それでも基本の機能には変わりがない.

#include <type_traits>
#include <utility>
#include <cassert>

// #1
template<class To, class From,
  typename std::enable_if<
    std::is_object<To>::value &&
    std::is_convertible<From, To>::value
  >::type* = nullptr
>
To restricted_implicit_cast(From && x) {
  return std::forward<From>(x);
}

// #2
template<class To,
  typename std::enable_if<
    std::is_lvalue_reference<To>::value ||
    (std::is_object<To>::value && std::is_move_constructible<To>::value)
  >::type* = nullptr
>
To restricted_implicit_cast(typename std::enable_if<true, To>::type && x) {
  return std::forward<To>(x);
}

// #3
template<class To,
  typename std::enable_if<
    std::is_lvalue_reference<To>::value
  >::type* = nullptr
>
To restricted_implicit_cast(typename std::remove_reference<To>::type &&) = delete;

// #4
template<class To, class From,
  typename std::enable_if<
    std::is_rvalue_reference<To>::value &&
    std::is_convertible<From, To>::value
  >::type* = nullptr,
  class = decltype( ::restricted_implicit_cast<To&>(std::declval<From&>()) )
>
To restricted_implicit_cast(From && x) {
  To &  t1 = x;
  To && t2 = std::forward<From>(x);
  assert( std::addressof(t1) == std::addressof(t2) );
  return std::forward<To>(t1);
}

#include <iostream>

int main()
{
  auto x = 0;
  auto const& ref = restricted_implicit_cast<double const&>(x);  // error, conversion to double const& from int& needs an implicit temporary
}

それぞれの定義を,順に解説したい.


まず #1 は,変換先が参照ではない場合の型変換だ.
これは通常の Perfect Forward を使っているので,分かっている人ならば特に問題はないだろう.


次に #2 は lvalue reference 版であり,また restricted_implicit_cast(0) のような変換に対応するものだ.
is_move_constructible で条件チェックしている以外は前回の記事の implicit_cast と同じなので,これも良いだろう.


その次の #3 では, C++11 の std::reference_wrapper の実装を参考に,

int const x = 0;
implicit_cast<double const&>(x); // ok
restricted_implicit_cast<double const&>(x); // error, because temporary is implicitly created

のような例を避けている.
これは, T && 型に対しては, T const& 型より T const&& 型の方が
多重定義解決において優先順位が高いことを利用したものだ.


最後の #4 は,おまけではあるが,変換対象が rvalue reference の場合に対処している.

int const x = 0;
restricted_implicit_cast<double&&>(x); // error, because temporary is implicitly created

その実装は
「変換元も変換先も lvalue にしたうえで restricted_implicit_cast すれば,まぁ一時オブジェクトは作られないだろう」
という楽観に基づいているため,例えば

template<class T>
class Hoge {
  T x;
  operator T const& () const& { return x; }
  operator T&& () && { return std::forward<T>(x); }
};

のようなクラスには対処できない(エラーになるべきではない場所でエラーになる)が,
そういう場合は素直に無印の implicit_cast を使えばいい,ということで.


で,こういうコードを書いてて,
これやっぱ,暗黙変換結果の参照束縛禁止機能を, attribute 辺りを使って言語組み込みでサポートするなり,
せめて restricted_implicit_cast 的な機能を持つ関数を標準ライブラリに用意しておくべきなんじゃね?
って思ったのでした.