いつの間にか GCC のオプションで -std=c++11 という書き方が可能になっていたので,
これからの C++11 関連の記事には C++0x ではなく C++11 というタグを付けることにします. *1
最近は GCC のみならず, Clang でも本格的に C++11 の機能が実装されるようになったし,
GCC は GCC で, Template aliases とか Non-static data member initializers とか Delegating constructors といった,
極めて分かりやすく恩恵も多い C++11 の機能が実装されてきているので,
そろそろ僕も最近 C++11 の記事をかけなかった分を取り戻そうかなぁとか.
C++11 では std::forward
という関数を使うことで Perfect Forward を実現していますが,
この std::forward
という関数は,別に Perfect Forward だけに使われるものではありません.*2
文量の都合上,詳しい説明は省きますが(詳しい勉強は各自で行なってください),
std::forward
という関数が何を行なっているかというと,要するに static_cast
です.
そう,実は std::forward
は Perfect Forward の他,キャストにも使えるのです.
具体的には,
std::forward<Arg>(arg)
というコードがあって,このコードが問題なくコンパイルできる場合には,このコードは
static_cast<Arg&&>(arg)
と機械的に書き換えることが可能です.
このとき, Arg
の型が T&
で表される参照型*3だった場合には, reference collapsing *4によって,このコードは
static_cast<T&>(arg)
と同じになります.
ここで使われる static_cast
は,通常の Perfect Forward では同じ型への変換であり,
そうでなくても基本的には明示的に static_cast
する必要のない暗黙変換なので,特に問題はありません.
一方で, Arg
の型が参照でない型 T
((もしくは rvalue reference T&&
))の場合には,このコードは
static_cast<T&&>(arg)
と書いたのと同じになります.
この static_cast
は,従来の「 void*
から一般のポインタへのキャスト」や「基底クラスから派生クラスへのキャスト」とは違い,
「左辺値から右辺値へのキャスト*5」,すなわち move を行っています.
これは C++11 で追加された機能で,ここでは「左辺値 lvalue 」や「右辺値 rvalue 」といった用語に対する詳しい説明は省きますが,
C++11 で move semantics を扱う場合,特に move を行う場合には,
最終的に static_cast
を使ってキャストを行う必要があります.
さて,そんな static_cast
ですが, C++11 からは move にも使われるようになったからといって,
従来の機能が使えなくなったわけではありません.
// C++11 でも,従来のように static_cast は使える int i; void* vp = static_cast<void*>(&i); // void* へキャスト,本来は要らないが書いても問題ない int* p = static_cast<int*>(vp); // void* から int* へキャスト // いつも可能とは限らないので明示的なキャストが必要 Derived d; // Derived は Base から派生しているものとする Base& b = static_cast<Base&>(d); // 派生クラスから基底クラスへのキャスト // 本来は要らないが書いても問題ない Derived& r = static_cast<Derived&>(b); // 基底クラスから派生クラスへのキャスト // ここには明示的なキャストが必要
つまり, move の為だけに static_cast
を使っているつもりでも,
うっかりミスによって,これらの従来の使用ケースに誤爆してしまうケースが存在する,ということです.
Base b; f( static_cast<Derived&&>(b) ); // プログラマは move の為にキャストしているつもりだったとしても, // 基底クラスから派生クラスへのキャストも同時に行われてしまう // これをコンパイル時に検出することは,このままでは難しい
これは危険だ,ということで, C++11 の標準ライブラリには, move を(比較的)安全に行うための機能が存在しています.
それが std::move
と std::forward
で,実際のプログラミングでは,専ら こちらを使うことになります.((ちなみに,従来の static_cast
の使用法が move に誤爆するケースも無視できない筈なのですが,そちらのケースに関しては,特に標準ライブラリでフォローされているわけではありません. 正直,片手落ちだと思うのですが….))
std::move
に関しては,今回の本題では無いので省略させて頂くとして,
std::forward
は,対象の型を明示してキャストを行う, static_cast
に極めて近いものとなっています.
先程, std::forward
は,コードが問題なくコンパイルできる場合には static_cast
と同じだ,と書きましたが,
この「コードが問題なくコンパイルできる場合には」というのが大事で,
要するに std::forward
は,コードに直接 static_cast
と書くと問題が起きる時に
きちんとコンパイルエラーにしてくれる機能を持っている関数であり,
Perfect Forward の時 以外にも, move semantics を使って ごにょごにょ したい場合には,大変に役立つものです.
具体例を,一つ挙げましょうか.
ある値をメンバとして保持する,簡単なラッパークラスを書きたい場合を考えます.
template <class T> struct wrapper { wrapper() : value_() {} wrapper( T src ) : value_( std::move(src) ) {} T & get() { return value_; } T const& get() const { return value_; } T && move() { return std::move(value_); } private: T value_; };
こんな感じです.
このようなクラスが何の役に立つのか,と思うかもしれませんが,
例えば int 型の変数を扱う場合に,このようなクラスを使うと初期化漏れがなくて 結構便利です.
他,タグをテンプレートパラメータとして持たせて,型レベルで区別させたい場合とかも便利です.
さて,このクラスを参照に拡張したい場合を考えます.
その場合,このクラスをそのまま使うと, std::move
周りでエラーになるはずです.
これは,この場合の変数 value_
は実は参照なのにも関わらず, std::move
は対象の引数を
値と解釈して問答無用に rvalue へとキャストするため,型の不一致が起こるからです.
そのようなケースでは 通常は特殊化を使って対処するのですが, std::forward
を使えば,
template <class T> struct wrapper { wrapper() : value_() {} wrapper( T src ) : value_( std::forward<T>(src) ) {} T & get() { return value_; } T const& get() const { return value_; } T && move() { return std::forward<T>(value_); } private: T value_; };
これで問題なく使えるようになります.
そうなる理由は,読者への宿題とします(説明するのが面倒になったらしい).
とまぁ,このように,中々に std::forward
は便利なので
皆さんも std::forward
を使いこなせるようになると, C++11 でのプログラミングの幅が広がるかと思います.
// とはいえ,素人の浅知恵で rvalue 周りを触ると dangling reference とかで容易に undefined behavior しますので,
// きちんと理解している人以外は素直に Perfect Forwarding だけに使うほうが賢明だったりしますが….
* * *
〜 解説記事とマニア向け記事の境界 〜
* * *
さて,そんな便利な std::forward
さんですが,こいつは以下のようなシグネチャを持っています.
namespace std { template <class T> T&& forward( typename remove_reference<T>::type& t ) noexcept; template <class T> T&& forward( typename remove_reference<T>::type&& t ) noexcept; // ただし T が U& の形で表せる場合( lvalue reference type の場合)には // 後者( && を引数に取る版)は ill-formed となる }
このうち,前者は要するに条件付きの move であり,
与えられた型 T
が参照(正確には lvalue reference )でない場合に限り,
引数として渡された変数を右辺値(正確には xvalue )へとキャストするものです.
std::string s = "hoge"; f( std::forward<std::string&>(s) ); // T は参照なので f(s) と同じ. s は move されない f( std::forward<std::string>(s) ); // T は参照でないので f( std::move(s) ) と同じ
このサンプルコードからは分かりにくいですが,これは要するに通常の Perfect Forward と同じです.
template <class T> void f( T && x ) { g( std::forward<T>(x) ); } int main() { std::string s = "hoge"; f( s ); // f のテンプレート引数 T は std::string& に推論されるので, // f の中の std::forward でも s の中身は move されない f( std::move(s) ); // f のテンプレート引数 T は std::string に推論されるので, // f の中の std::forward で s の中身は move される }
一方,後者の形の std::forward
を使うと,
std::string f(); // std::string の値( prvalue )を返す関数 std::forward<std::string>( f() ); // 後者の形の forward が有るので問題なくコンパイルできる // std::forward<std::string&>( f() ); // ダメ. rvalue は lvalue に変換できない
このように,右辺値は右辺値のまま返しつつ,右辺値から左辺値への変換を防げます(が,正直,あんまり使われないです).
さて, std::forward
に後者のような形のシグネチャが存在するのは,
要するに std::forward
が「安全なキャスト」に他ならないからで,
不自然な変換でさえなければ,受け取る引数を制限する必要はないからなのですが*6,
実を言うと,この形のシグネチャが存在すると,困ったことが起きてしまいます.
以下のコードを見てください.
std::string s = std::forward<std::string>(x);
このコードは一見して, std::string
型の変数 x
の値を move して
新しい std::string
型の変数 s
に格納しているように見えますが,
実際には(変数 x
の型によっては)そういう動作は行われない場合があります.
もちろん,変数 x
の型が std::string
ならば,これは期待通りに move が行われます.
関数 std::forward
のシグネチャは,
std::string&& forward( std::string& t ) noexcept; std::string&& forward( std::string&& t ) noexcept;
となるので,オーバーロード解決で前者が選ばれ,引数の move が行われるからです.
しかし,変数 x
の型が std::string
へと暗黙変換できる型,例えば
char const*
の場合には,関数 std::forward
のオーバーロード解決で
引数の move が行われる前者の関数が選ばれることは,決してありません*7.
一方,後者の関数は, x
から std::string
へ暗黙変換した一時オブジェクトを
束縛することで呼び出せるため,結果として後者の関数が呼ばれることになります.
しかし,その場合, forward
から返されるのは, x
をコピー((この場合には x
は move されてないので move ではなく copy になります))して出来た一時変数への参照であり,
x
が std::string
型だった場合に返される,変数 x
への参照をrvalue へとキャストしたもの
(平たく言うと, x
を move したもの)とは全く別のものなのです.
もちろん,このような予想外の動作は, std::forward
を通常の Perfect Forward に使う限りでは起きません.
しかし一方で,通常の Perfect Forward に使うだけならば,それこそ後者の形の forward
は必要ないので,
正直な話,僕には std::forward
に rvalue を取る形が存在する意義を理解できなかったりします.*8
なので,俺々関数を定義することを厭わないならば,引数の forwarding (と rvalue 関連のキャスト)は,
std::forward
の後者の形が一律に = delete;
された
namespace oreore { template <class T> constexpr T&& forward( typename remove_reference<T>::type& t ) noexcept { return static_cast<T&&>(t); } template <class T> T&& forward( typename remove_reference<T>::type&& t ) = delete; }
という俺々関数を定義して使うのが良いかも知れません.
俺々関数ならば constexpr
にする(( move なのに constexpr
って? と思うかもしれませんが, move では constexpr
性は失われないので安心です))ことも可能ですし(( libstdc++ の move
や forward
は constexpr
ですが,標準では constexpr
指定されていません(これは恐らく defect なので,後で標準も constexpr
に修正されると思います))),
これなら一時変数の寿命に関する問題も起きにくいですので.
もちろん,標準の std::forward
でも,普通に使う分には まず問題は起きないので,
下手な黒魔術は止めて知っていることだけを書く,というのも,普通に優秀な解決策だと思います.
// ちなみに oreore::forward
で二番目の形を明示的に = delete;
しているのは,
// lvalue reference に rvalue を束縛できてしまうことで悪名高い VC++ への対策の他,
// T
が const
の場合に一時変数を束縛できてしまうのを防ぐ意味もあります.
続くかも.
*1:参考: http://d.hatena.ne.jp/gintenlabo/20110903/1315059927
*2:以前にも そのような趣旨の記事 http://d.hatena.ne.jp/gintenlabo/20110110/1294683111 を書きましたが,この記事では 他の面に注目します.
*3:厳密には 左辺値参照 lvalue reference
*4:参照の参照を単なる参照に変換してくれる C++11 の機能. 詳しくは http://d.hatena.ne.jp/gintenlabo/20100916/1284657258
*5:厳密には lvalue から xvalue へのキャスト
*6:多分. 詳しく議論を追ったわけではないので間違ってるかもしれません. 識者の意見を求む
*8:存在する理由を知っている人がいるなら,是非とも聞いてみたいものです. 大事なことなので2回言いましたよ.