右辺値参照~完全転送まで100%理解するぞ! part7 可変引数テンプレート
前回の
template<class T> void hoge(T&& x){ std::forward<T>(x); }
universal referenceを使うことで、右辺値参照も左辺値参照もどちらも入ってくる
std::forwardを使うことで、右辺値参照の場合は ムーブコンストラクタ、左辺値参照の場合は コピーコンストラクタ が実行される
これでほぼ完ぺきだけど 完全転送と呼ぶには hoge(x,y,z...) と、可変引数に対応しなければいけません!
可変引数テンプレート
まずは可変引数テンプレートの説明
可変引数(printfのような va_list)のような書式で コンパイル時に展開される
#include<iostream> using namespace std; void bar( int i, string& s ){ cout << i << s << endl; }; template<class... T> void hoge(T... t){ bar(t...); } auto main() -> int { hoge(1, string(" test") ); // hoge(1); // hoge(int) は宣言されていないので エラー return 1; }
上記コードの template<class... T> で、0個以上の引数をもつものにヒットする bar(t...); で、tを展開したパラメーターを引数にもつ barを呼ぶ
barに必要なだけオーバーロードすればいい
あとはこれを universal reference と std::forwardを使い
#include<iostream> using namespace std; void bar( int i, string s ){ cout << i << s << endl; }; template<class... T> void hoge(T&&... t){ bar(std::forward<T>(t)...); } auto main() -> int { string s("s"); hoge(1, s ); hoge(1, " test" ); // hoge(1); // hoge(int) は宣言されていないので エラー return 1; }
これで、どんな引数でも完全転送が出来ました!
ま、ビットフィールド等 工夫しなければ完全転送できないパターンもあるようですが 一般的には これでOK
クラスのコンストラクタを可変長テンプレートにして ごにょごにょ
前回なやんだ続きではあります
関数テンプレートは暗黙的インスタンス化可能だが、クラステンプレートは不可能 ならば コンストラクタを可変長テンプレートすればいいじゃない
#include<iostream> #include<sstream> using namespace std; struct Hoge{ void stats_(){}; stringstream ss_; template <class Head, class... Tail> void stats_(Head&& head, Tail&&... tail){ ss_ << head; stats_(std::forward<Tail>(tail)...); } template <class... T> Hoge (T&&... t){ stats_(std::forward<T>(t)...); } friend ostream& operator << (ostream& os, const Hoge& hoge); }; ostream& operator << (ostream& os, const Hoge& hoge){ os << hoge.ss_.str(); return os; } int main() { cout << Hoge("aaa,", "bbb,", "ccc,") << endl; }
つまり 関数オブジェクトは
#include<iostream> #include<sstream> using namespace std; struct Hoge{ void stats_(){}; stringstream ss_; template <class Head, class... Tail> void stats_(Head&& head, Tail&&... tail){ // std::cout << head; ss_ << head; stats_(std::forward<Tail>(tail)...); } template <class... T> Hoge& operator() (T&&... t){ stats_(std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream& os, const Hoge& hoge); }; ostream& operator << (ostream& os, const Hoge& hoge){ os << hoge.ss_.str(); return os; } int main() { cout << Hoge()("aaa,", "bbb,", "ccc,") << endl; }
こうやればできたんだなーー
関数オブジェクトでは 自分を返して汎用的にしてみたけど、何を返すのが正しいのかは わからない
関数テンプレートは暗黙的インスタンス化できる
で終わりなんだけど、少し悩んでやっぱ無理そうだったのでメモ 関数テンプレートで実装出来るのだが、使い勝手や最適化など考えて 関数オブジェクトにしたかったが 関数とクラスの テンプレートの違いで出来なかった
追記:やっぱできたぜ。メンバをテンプレートにすればOK
クラスのコンストラクタを可変長テンプレートにして ごにょごにょ - ムラサメ研究ブログ
テンプレート関数は暗黙的インスタンス化可能です!
template<T> void hoge(T t){}; hoge(12);
可能というかむしろ、暗黙的に使うよね。
もちろん あえて明示的にも書ける
template<T> void hoge(T t){}; hoge(12);
クラスは暗黙的インスタンス化できない
#include<iostream> template <class T> struct Stats{ void operator()(T t){} const; }; int main() { Stats<int>()( 12 ); // Stats()( 12 ); // 暗黙的インスタンス化できない }
右辺値参照~完全転送まで100%理解するぞ! part6 Universal Referenceと std::forward
やっと UniversalReferenceですよ!
その前に復習
右辺値参照を引数にする時は あまり気にせず下記のようにするのが正しいのでしたね
#include <iostream> using namespace std; class X{ public: // explicit付けられない! X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } }; void hoge(X x){ cout << "hoge(X)" << endl; } int main() { X x; hoge(x); hoge(move(x)); }
呼ぶときに std::move() しなければコピーコンストラクタ、つければムーブコンストラクタが働く これは 暗黙的変換を利用し XとX&& を受け入れています
でも、コピーせず左辺値参照を渡したい事多いよね? コピーするだけ無駄だし それに STLのなかには explicit付きのものしか呼べない関数もあったりで 明示的に行いたい
そんな時は愚直に 左辺値参照と右辺値参照の関数を2つ(const参照いれて3個)実装すればよい
#include <iostream> using namespace std; class X{ public: // explicit付けられる。付けれるなら絶対付けた方がよい explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } }; void hoge(X& x){ cout << "hoge(X&)" << endl; } void hoge(const X& x){ cout << "hoge(const X&)" << endl; } void hoge(X&& x){ cout << "hoge(X&&)" << endl; } int main() { const X x; hoge( x ); hoge( std::move(x) ); }
このようにすれば、当然 右辺値の時と左辺値の時で処理をわけられる
しかし、毎回同じような関数が3個。引数が増えればさらに倍増していく
それを防ぐのが Universal Referenceである
Universal Reference
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } X& operator =(X& x){ if(&x==this) return(*this); cout << "copy assignment" << endl; return(*this); } X& operator =(const X& x){ if(&x==this) return(*this); cout << "const copy asignment" << endl; return(*this); } X&operator =(const X&& x){ if(&x==this) return(*this); cout << "move asignment" << endl; return(*this); } ~X(){ cout << "destuct" << endl; } void foo(){}; }; class Hoge{ public: template<class T> void hoge(T&& x){ // x_ = x if lvalue // x_ = std::move(x) if rvale cout << "hoge(T&&)" << endl; } private: X x_; }; int main() { Hoge h; cout << "____" << endl; { X x; h.hoge( x ); } cout << "____" << endl; { const X x; h.hoge( x ); } cout << "____" << endl; { X x; h.hoge( std::move(x) ); } cout << "____" << endl; { h.hoge( X() ); } cout << "____" << endl; { h.hoge( std::move(X()) ); } cout << "____" << endl; } constuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ destuct
template
そして 関数テンプレート void hoge(T&& x) に Tが X&の時とX&&の時を考える
ここは 前回の 参照の圧縮を使えば TがX&の時は hoge(X& x)
TがX&&の時は hoge(X&& x) となる
ところが問題はコメント部分である 引数 xが 左辺値(X&) の時は copy assignment(X& operator =(X&)) を行いたいが 右辺値(X&&)の時は move assignment(X& operator =(X&&)) を行いたい
そんな時に使えるのが std::forward
実際に使ってみる
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } X& operator =(X& x){ if(&x==this) return(*this); cout << "copy assignment" << endl; return(*this); } X& operator =(const X& x){ if(&x==this) return(*this); cout << "const copy asignment" << endl; return(*this); } X&operator =(const X&& x){ if(&x==this) return(*this); cout << "move asignment" << endl; return(*this); } ~X(){ cout << "destuct" << endl; } void foo(){}; }; class Hoge{ public: template<class T> void hoge(T&& x){ x_ = std::forward<T>(x); cout << "hoge(T&&)" << endl; } private: X x_; }; int main() { Hoge h; cout << "____" << endl; { X x; h.hoge( x ); } cout << "____" << endl; { const X x; h.hoge( x ); } cout << "____" << endl; { X x; h.hoge( std::move(x) ); } cout << "____" << endl; { h.hoge( X() ); } cout << "____" << endl; { h.hoge( std::move(X()) ); } cout << "____" << endl; } constuct ____ constuct copy assignment hoge(T&&) destuct ____ constuct const copy asignment hoge(T&&) destuct ____ constuct move asignment hoge(T&&) destuct ____ constuct move asignment hoge(T&&) destuct ____ constuct move asignment hoge(T&&) destuct ____ destuct
std::forwardを使えば、T&、const T&、T&& と3つの引数のオーバーロードを1関数で処理できる
とてもすばらしい。
右辺値参照~完全転送まで100%理解するぞ! part5 参照の圧縮(reference collapsing)
右辺値参照の山場の一つ Universal Referenceまであと一歩
参照の圧縮とは
UniversalReferenceそのものだが
参照は(const等のストレージクラス除き)2種類しかない
X& (左辺値参照)、X&&(右辺値参照)
X&&& 等はエラーですし X& && 等、参照を重ねる事は出来ない
でも、templateを使うと不運にも参照が重なる事がある
template<class T> void hoge(T& t){ }
例えばこのようなテンプレートに X&型の変数を適用したら T&は X& & になり、参照が重なってしまう これらの参照が重なった時はどうなるのだろうか?
コンパイルエラーにすると、テンプレートに参照を渡すものがほとんど動かなくなる C++では当然 参照が重なった場合に明確な法則がある
#include <iostream> using namespace std; class X{ public: X(){ } X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } }; int main() { using Y=X&; using Z=X&&; X x; X( (Y&)x ); X( (Y&&)x ); X( (Z&)x ); X( (Z&&)x ); } ---- copy copy copy move
template 以外に typedef (using) でも参照を重ねる事が出来るので 試してみた
結果は X& & -> X&
X& && -> X&
X&& & -> X&
X&& && -> X&&
と、結論からいえば すべて右辺値参照の時のみ右辺値参照。それ以外は左辺値参照となる
このことを 参照の圧縮(reference collapsing) と呼ぶ
右辺値参照~完全転送まで100%理解するぞ! part4 Universal Reference その前に
こうやって 人に見せようと書く事は自分の理解になる
復習DEATH
前回の復習。右辺値参照を関数の引数にするには 一見コピーに見えるが void hoge( X x ) とプロトタイプすればよい 呼び出し側で hoge( std::move(x) ) とすればムーブコンストラクタが使われる という事 で 復習
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } ~X(){ cout << "destuct" << endl; } }; void hoge(X x){ cout << "hoge(X)" << endl; } int main() { { X x; hoge( x ); } cout << "____" << endl; { const X x; hoge( x ); } cout << "____" << endl; { X x; hoge( std::move(x) ); } cout << "____" << endl; { hoge( X() ); } cout << "____" << endl; { hoge( std::move(X()) ); } } constuct copy hoge(X) destuct destuct ____ constuct const copy hoge(X) destuct destuct ____ constuct move hoge(X) destuct destuct ____ constuct hoge(X) destuct ____ constuct move hoge(X) destuct destuct
std::move() で呼ぶとムーブされ、何もつけないとコピーされる。正しい使い方だ。 少し気になるのは X() と右辺値で直接呼んだ場合は一時オブジェクトが生成されず効率的だが std::move( X() ) とすると、一時オブジェクトを生成し、それを右辺値にキャストしムーブコンストラクタが発生する (最適化で このへんは最適化されると思うけど、実装依存なので 可能なら コードで最適化しておきたい)
本題
上のコードで納得いかないところ オブジェクトの参照だけで良い時にも無駄にオブジェクトのコピーが走る 納得いかないので 調査をする
前回は一緒に受け取るようにした 参照、const参照、右辺値参照に分割してみる (先にこちらから説明すべきであった 反省)
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } X& operator =(X& x){ if(&x==this) return(*this); cout << "copy assignment" << endl; return(*this); } X& operator =(const X& x){ if(&x==this) return(*this); cout << "const copy asignment" << endl; return(*this); } X&operator =(const X&& x){ if(&x==this) return(*this); cout << "move asignment" << endl; return(*this); } ~X(){ cout << "destuct" << endl; } void foo(){}; }; static X xx; /* void hoge(X x){ xx = x; cout << "hoge(X)" << endl; } */ void hoge(X& x){ xx = x; cout << "hoge(X&)" << endl; } void hoge(const X& x){ xx = x; cout << "hoge(const X&)" << endl; } void hoge(X&& x){ xx = std::move(x); cout << "hoge(X&&)" << endl; } int main() { cout << "____" << endl; { X x; hoge( x ); x.foo(); } cout << "____" << endl; { const X x; hoge( x ); } cout << "____" << endl; { X x; hoge( std::move(x) ); } cout << "____" << endl; { hoge( X() ); } cout << "____" << endl; { hoge( std::move(X()) ); } cout << "____" << endl; }
面倒なので static変数にコピーしているが、普通はクラスのメンバに代入するだろう (explicit付けたよ! 前回の例では つけられないが ムーブコンストラクタにはexplicit付けるべき)
左辺値参照が引数に入ってきたら、中身を自分にコピー 右辺値参照であればムーブ
バッチリ!!
ところがこれを前回のように hoge(X x) で両方を受け取ると困った事になる
xx = x; と書けば ムーブ出来る時でもコピーになるし xx = std::move(x); では、常にムーブになる
ではやはり、 参照、const参照、右辺値参照 と常に3種類の関数が必要なのか??? また 引数がふえれば 6種類、9種類・・・ 組み合わせ爆発してしまう
そこで登場するのが Universal Reference
次回ご期待!
右辺値参照~完全転送まで100%理解するぞ! part3 関数呼び出し
いまいちまだ 納得していない部分もある(理解不足
実践的な関数呼び出し
右辺値参照はムーブセマンティクスのために使われる ムーブセマンティクスは主に ・ 所有権の移動 ・ コピーのコストを削減(ポインタのすげ替え)
で使われる
所有権の移動
コピーされたら困る(インスタンスが複数になってはダメ)ものに 例えば スレッドや、ユニークポインタ等で使う
#include <iostream> #include <thread> using namespace std; int main() { thread thread_; // ムーブコンストラクタが呼ばれるので threadは2個出来ない thread_ = thread( [] { cout << "done" << endl; }); // コピーコンストラクタ不可能 // thread thread2_ = thread_; if (thread_.joinable()) thread_.join(); }
上記コードは std::threadは コピーコンストラクタがdeleteされ、ムーブコンストラクタしか無い
thread = thread( [] { cout << “done” << endl; });
は、右辺値参照であるため、ムーブコンストラクタが呼ばれ、結果的に threadの所有権が threadに渡るので
1個しかスレッドが作成されない
もし仮に コピーコンストラクタで動いた場合は 一時オブジェクトとしてスレッドが作成され、さらに同じものを thread_にコピーし スレッドは2個作成されるものと思われる
thread thread2 = thread;
は、もちろんコピーコンストラクタは無いため コンパイルエラーになる
同じく unique_ptrもコピーコンストラクタが無く、ムーブコンストラクタのみ許可されているため ポインタが2個になる事はない
コピーのコストを削減
こちらが、よく使う方。
#include <iostream> using namespace std; class X{ public: X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } }; int main() { X x; X x1 = x; // lvalue X x2 = move(x); // rvalue X x3 = move(X()); // rvalue } copy move move
復習になるけど、lvalueとrvalueにより コピーとムーブになる
このことを踏まえ 正しい関数呼び出しは
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } ~X(){ cout << "destuct" << endl; } }; X hoge(X x){ return( move(x)); } int main() { X x; hoge(move(x)); } constuct move move destuct destuct destuct
上記が正しい関数プロトタイプである
実際には hogeのreturnには std::moveを書かなくとも、moveしてくれるので書かないのが普通である
ただし、見てわかるとおり hogeを呼ぶ際に std::moveを忘れて hoge(x) とすると
constuct copy move destuct destuct destuct
と、当然のように コピーコンストラクタが走るので、使うときによく確認しなければならない
もちろん、コピー禁止にするなら hogeの引数を X&& にすればよい
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } ~X(){ cout << "destuct" << endl; } }; X hoge(X&& x){ return( move(x)); } int main() { X x; hoge(move(x)); } constuct move destuct destuct
と、さらに処理が速くなり、左辺値の場合はコンパイルエラーになるので、ムーブオンリーにできる
まとめ
大体の場合は、関数プロトタイプでは 実体を渡せばいい X hoge(X x) 関数を呼ぶときに 右辺値であればムーブ、左辺値であればコピーされる コピーしたくない時は、呼ぶときに ムーブセマンティクス忘れないように!