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_wrapper
や static_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
である場合に対処してなかったり, noexcept
や constexpr
にも対応してないなど
不足はあるが,それでも基本の機能には変わりがない.
#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
のような変換に対応するものだ.
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
的な機能を持つ関数を標準ライブラリに用意しておくべきなんじゃね?
って思ったのでした.