std::forward restriction

いつの間にか GCC のオプションで -std=c++11 という書き方が可能になっていたので,
これからの C++11 関連の記事には C++0x ではなく C++11 というタグを付けることにします. *1
最近は GCC のみならず, Clang でも本格的に C++11 の機能が実装されるようになったし,
GCCGCC で, 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::movestd::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 になります))して出来た一時変数への参照であり,
xstd::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++ の moveforwardconstexpr ですが,標準では constexpr 指定されていません(これは恐らく defect なので,後で標準も constexpr に修正されると思います))),
これなら一時変数の寿命に関する問題も起きにくいですので.


もちろん,標準の std::forward でも,普通に使う分には まず問題は起きないので,
下手な黒魔術は止めて知っていることだけを書く,というのも,普通に優秀な解決策だと思います.

// ちなみに oreore::forward で二番目の形を明示的に = delete; しているのは,
// lvalue reference に rvalue を束縛できてしまうことで悪名高い VC++ への対策の他,
// Tconst の場合に一時変数を束縛できてしまうのを防ぐ意味もあります.


続くかも.

*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:多分. 詳しく議論を追ったわけではないので間違ってるかもしれません. 識者の意見を求む

*7: Visual C++ は無視するものとします

*8:存在する理由を知っている人がいるなら,是非とも聞いてみたいものです. 大事なことなので2回言いましたよ.