メイドでもよく分る右辺値参照
本日2014年
2011年から早3年。C++11も浸透してきた、してきてる、してきて欲しいなという時分ですね
冬椿です。ファミレスに行ったらカレーがメニューから消えてました。こんばんは
C++11で追加された機能の一つに『右辺値参照』というものがあります
こいつは裏で、つまりライブラリ内の実装などで使われることは多くても
実際使ったことがあるという人は少ないのではないしょうか?
右辺値参照が必要になった経緯
例えばこのような関数があったとする
List make_big_List() { List temp; for (size_t i = 0; i < 2011; ++i) temp.push_back(i); return temp; }
2011とはとても大きい数だ
ゆえに、tempも非常に大きいことが予想される
このコードはC++11以前であると
tempの中身をコピーする→tempを削除する
という動きになっていた
しかしこれは無駄であり、しかもコストがそこそこ大きい
古代人はこれを避けるためこのようなコードを書いたりしてこれを忌避していた
void make_big_List(List&temp) { for (size_t i = 0; i < 2011; ++i) temp.push_back(i); }
すっげぇ手続き的なコードだな!!まるでアセンブラのようだ!!
上記のコードはconstが使えなかったりとか適当なデフォルトコンストラクタがない場合とかいろいろな場合で不都合があった
速度問題さえ解決できれば前者のコードの方が取り回しが効いて便利である
そこで、「tempはどうせ削除されるのだからtempの中身を移してしまおう」と考えた
この時、普通のコピーコンストラクタであるとconstがついているのでtempを変更できない
というかtempが近々削除される中身を移してよいオブジェクトか、否か判別できない
そこで
「これは近々削除されるなどの理由で中身をはぎ取っても問題ないオブジェクトである」
ということを表すために右辺値参照が作られた
ムーブコンストラクタについて
まず、右辺値参照の最終目標であるムーブコンストラクタについて説明して行きたい
ムーブコンストラクタとはなんであろうか?
それはムーブ(移動)するコンストラクタである
コピーコンストラクタはコピーするコンストラクタ
ムーブコンストラクタはムーブするコンストラクタ
サンプルを見てみよう
struct Box { using type = std::array<int, 2011>; //デフォルトコンストラクタ。深い意味はない。大きいデータをnewできればよかった Box():p(new type){} //コピーコンストラクタ。pをディープコピーしている Box(const Box&a) :p(new type(*(a.p))){} //ムーブコンストラクタ //前述の通りaは右辺値参照であり中身をはぎ取っても問題ないので //pを移し替えている Box(Box&&a) :p(a.p){a.p=nullptr;} ~Box(){ delete p; } type*p; };
最終的にはこういうことができるようになりたい
そんなわけで右辺値参照の機能と意義を考えていきたい
参照を真面目に考える
右辺値参照ではない。普通の参照の話をまずしよう
普通の参照とは、これである
int &x
さっきも出てきた。日常的によく使われる、普通の参照である
この参照、実は名前を「左辺値参照」という
C++11以前もそのように呼んでいたかについては実は知らない。右が出てきて初めて左を意識した
ところであなたはこの左辺値参照に代入できるモノ出来ないモノの違いを述べることはできるであろうか?
これは私の勘だが、おそらく貴方は右辺値参照以前に参照を理解していない
間違ってたらゴメン。お詫びにうちの山羊をファックしていいよ
超ざっくりいえば
X = Y
っていう代入式のXの方になれるモノが左辺値参照に代入できる
つまりだいたい変数か参照型
std::vector<int> v(3); int x; x=3;//変数はXの方になれる v.at(0)=4;//参照はXの方になれる //(x+v.at(0))=9;//出来ない
代入式の左にあるから、左辺値
左辺値を参照できるから左辺値参照
const 参照については後で話す
右辺値参照とは?
左辺値参照について解説したこの時点で勘のいい人ならば
既に右辺値参照がなんなのかもうお気づきであろう
代入式の右にあるから、右辺値
右辺値を参照できるから右辺値参照
本当に、本当に機能的な本質はこれだけなのだ(細かい補足が少しあるが)
右辺値について
前述の通り右辺値とはYの側になるモノである
ただしいくつか捕捉がある
まず右辺値に左辺値は含まない
参照や普通の変数は左辺値であり、右辺値ではない
誤解を恐れずに言うならば左辺値とは変数であり、右辺値とは値であると自分は考えている
右辺値の例を挙げると
x+yやf()のような演算結果(ただし参照型の場合を除く)とか42(リテラル)とかである
右辺値参照について
*1
ようやく右辺値参照の話に入れた。まずはサンプルだ。サンプルは大事だ。百聞は一見にしかずって言うしな!!
int &&x = 3; int a = 4; int b = 2; int &&y = a + b; int &&z = sin(3.14*1.5);
右辺値参照に突っ込まれる右辺値。初見のインパクトが結構でかい
「聞いてはいたけど、うわまじで入ってるよ」って気分になる
そして右辺値、つまり演算結果等は事実上使い捨てのオブジェクトである
a + bという演算の結果、メモリのどっかに生成されたオブジェクトは基本的に変更されることがないし
参照されるのも一度きりである
//a+bの結果は変更されることはないし、(+ c)の計算以外で使われることもない
(a + b) + c;
なのでその演算結果の中身をはぎ取っても問題がない
それを参照する右辺値参照の値の中身ははぎ取ってもよいのである
右辺値参照に右辺値参照の変数は入れられない
言葉にすると微妙に分かりずらいが要するに
int &&x=3; int &&y=x;//エラー!! int &&z=f();//fはint&&を返す関数
こういうことである。xそのものは変数であり型に関係せず左辺値である
何故ならば「右辺値参照型である」ということは右辺値左辺値の決定に何ら影響しない
普通の変数など左辺値を右辺値参照にぶち込む方法
int &&y=x;
というのが出来ないのは前述の通りである
xがintでもint&でもint&&でも、だ
しかし落胆するのは早い。左辺値を右辺値参照に入れる方法は存在する
上記の通りint&&を返す関数は右辺値参照に入れることができる
要するに
int&&z=f(x);//fはxを丸投げする
このような関数があればよいのだ
この機能を持った関数が標準ライブラリ
forward(C++11) - cpprefjp - C++ Library Reference
これは受け取った左辺値をT&&に変換して返す関数である
int &&y = std::move(x);
これを用いることで左辺値を右辺値参照に入れることができる
そしてこれを用いることでムーブコンストラクタなどを左辺値に対して使うことができるようになる
box a; box b=a; //これはコピーコンストラクタが動く box c=std::move(a); //これはムーブコンストラクタが動く
左辺値をmoveすることの持つ意味
左辺値をmoveしたとき
その左辺値は死んだと考えなければならない
ムーブすることはつまり中身の所有権の放棄である。中身の所有権は移動し、そのクラスは死んだのだ
box a; box b=std::move(a); //これはムーブコンストラクタが動く box c=std::move(a); //ワーニング!!文法上不正ではないが死体をムーブしようとしている!!
一回目のムーブでaは死んだのだ。ゆえにmoveしてはならない
プログラマは、つまりあなたはmoveする際うっかりまだ使う変数を殺してしまわないように注意を払わなければならない
殺してよいのは用済みだけである
貴方は細心の注意を払い丁寧に変数を殺す義務がある
また死体の中身すっからかんであることを期待してもならない
例えばvector等であればmove後sizeとcapacityが0になることをなんとなく予想してしまうが
所有権を放棄したからと言って中身を所有していないとは限らない
実装によっては中身をすっからかんにするより高速であったりする場合持ち続けるかもしれない
この辺りは未規定動作であり、つまりある程度自然であれば好きに計らって良いことになっているので
そもそも死体に何か期待しないほうが良いというのが自分の意見である
ただし、move後のオブジェクトに代入などはできる
代入はオブジェクトに何一つ期待していないからである
box a; box b=std::move(a); //これはムーブコンストラクタが動く a=box(); //OK
代入はaの状態に限らず成功する
std::moveの使い時、使い道
実はstd::moveを使うタイミングというのはほぼない
というのは最もムーブしたい以下のような場合は最適化されて自動でムーブコンストラクタが呼ばれるからだ
List make_big_List() { List temp; for (size_t i = 0; i < 2011; ++i) temp.push_back(i); return temp;//←ここ }
暗黙の内にList(List&&)が呼ばれる
いやもしかしたらもっと最適化されてそれさえ呼ばれないかもしれない
moveするタイミングというのは本当にほとんどない
hogehoge x=std::move(a);
このような構文を書くときはmoveを必要になるが、あまり書かない
もしかしたら最もmoveを使うのはstd::unique_ptrを用いるときかもしれない
const 参照について
const参照は主に関数の引数で使う。その辺はそこら辺のC++参考書を読んでほしい
const参照の特別なところは「関数の引数になる」という超大役を引き受けたために
利便性の観点から右辺値も取れるようになったことである
なのでこいつが左辺値参照か?と聞かれると
分類は間違いなく左辺値参照だが機能的には左辺値参照?という感じになる
鳥でいうところのペンギン的なヤツである
const 右辺値参照について
Q:使い道あるの?
A:あるらしいよ?
const rvalue referenceは何に使えばいいのか - ここは匣
型とtemplateと右辺値参照
templateを扱うものにとって実は右辺値参照は避けて通れない問題でもあったりする
make_uniqueを実装してみる
make_uniqueとはmake_sharedのunique_ptr版であり、unique_ptrを作る為のへルパ関数である
なお、make_sharedのリファレンスはこちら↓
make_shared(C++11) - cpprefjp - C++ Library Reference
イメージとしては
make_unique<hoge>(1,2,3,4) -> std::unique_ptr<hoge>(new hoge(1,2,3,4))
となればよい。よいがもちろん引数が右辺値参照などの場合についても考える必要がある
まず宣言を書こう。宣言は大体make_sharedと同じだ
template <class T, class... Args> unique_ptr<T> make_unique(Args&&... args);
ここでおかしなことに気が付く
全て右辺値参照で受け取ろうとしていることだ
これには訳がある
template<class T> void f(T&&x);
テンプレート関数において以下のように変形される
f(3); //f<int>(x) int x; f(x); //f<int&>(x)
左辺値を入れた場合Tは参照型になるのだ
そして右辺値参照の参照、及び参照の右辺値参照は参照になるというルールがある
using r_ref=int&&; using l_ref=int&; r_ref&x; //←int& l_ref&&y; //←int&
よって
f(3); //f<int>(int&&x) int x; f(x); //f<int&>(int&x)
となるのである
これを用いることでmake_uniqueは
int x; const int y; make_unique<hoge>(1,x,y);//<hoge,int,int&,const int&>(int&&,int&,const int&)
と解釈されるのであり、延々とn番目が右左辺値参照の場合云々オーバーロードをしなくて済むのである
これでmake_unique完成だ!!
というわけにはいかない。以下今にも動きだしそうなサンプル
template <class T, class... Args> unique_ptr<T> make_unique(Args&&... args) { return unique_ptr<T>(new T(args...));//BAD!! }
一見問題なさそうだが
左辺値は右辺値参照に入れられない事を思い出してほしい
argsは型は左辺値参照か右辺値参照か不明だが間違いなく変数であり左辺値である
ゆえに右辺値参照に入れることはできない
しかし、ただmoveすればいいというものではない
return unique_ptr<T>(new T(std::move(args)...));//BAD!!
今度はただの左辺値まで右辺値参照にしてしまっている
このような場合もう一つのmove系関数を用いる必要がある
それがstd::forwardだ
forward(C++11) - cpprefjp - C++ Library Reference
forwardの目的は「安全な転送」
要するに上記のような場合を解決するためにある。forward以下のように使う
return unique_ptr<T>(new T(std::forward<Args>(args)...));//GOOD!!
forwardは右辺値参照にすべきものは右辺値参照に、左辺値参照にすべきものは左辺値参照にして返す関数である
これでようやくmake_uniqueの実装が完了した
template <class T, class... Args> unique_ptr<T> make_unique(Args&&... args) { eturn unique_ptr<T>(new T(std::forward<Args>(args)...)); }
実はもう一つmove系関数があるのだがこれについては記憶の隅にとどめておくだけでいいと思う
デストラクタ内など非常に特殊な状況下以外で使うことはない
forward(C++11) - cpprefjp - C++ Library Reference
type_traitsについて
type_traits(C++11) - cpprefjp - C++ Library Reference
ここを見てもらえばわかると思うが右左辺値参照をチェックしたり取り除いたり加えたりする関数がある
なおconst参照の項でも言ったが
[const T]参照
であり取り除くときは参照→const
付け加えるときはconst→参照の順番ですること
またこのブログではrvalue referenceという語を用いず右辺値参照という語で解説している
理由は3つ
ここは日本であること
ググラビリティとかの関係
あと書いたとき右辺値参照の方が字数が少なかったからである
*1:余談だが 右辺値とか右辺値参照とか言う名前、個人的にあまり好きではない 右辺値参照とはいうが後述の通り実質的に左辺値を握ることも多々ある 個人的には委譲参照とかそこら辺の名前が良かったのだがルールなので仕方がない