読者です 読者をやめる 読者になる 読者になる

すてにゃん氏の技術ぶろぐ

技術っぽいこと書きます

C++のmove semantics完全に理解した

C++

C++11といったらmove semanticsといっても過言ではない[要出典]ので調べてみた。

皆さんのご家庭にも置いてあるThe C++ Programming Language Fourth EditionとEffective Modern C++ を参考にした。

The C++ Programming Language (4th Edition)

The C++ Programming Language (4th Edition)

Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14

Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14

moveってなんすか。横文字わかんないっす

C++プログラマならコピーとかは知ってるよね。ムーブはコピーなんかよりも大抵の場合もっと効率いいらしいよ。え、よくわからないって?

例えばだ、今日は8月31日だけど夏休みの宿題全く手を付けていない。どうする?友達の宿題写す一択だよなぁ。
でもさ、プリント50ページとかあってさ、全部手書きで写すのって大変じゃない?タイムリミットは1日だよ?!無理じゃんー!
コピー機使えたらいいけどさぁ、そんなのバレるしインク代かかるし解決にはならない。
そんな時ムーブが役立つ!!
なんと!友達の宿題を奪い名前欄のみ変更して自分のものにする
完璧なプランだ。時間と労力が節約できてしかも先生にもバレない!
move semantics最高!!!

そう、C++でコピーする時ってでかいオブジェクトだと時間かかるしパフォーマンス的に痛い。ポインタ使ってゴニョゴニョするのもいいけど正直めんどくさい。
そういう時こそmoveを使おう!友達は宿題を失うけどまああんなヤツ最初からどうでもいいよなぁ。 1つだけ気をつけるべき部分はmove元のオブジェクトの操作は未定義なので極力ほっとくこと。だってさ、友達の宿題奪ったんだから友達はもう宿題を持っていないはず、操作できるわけないっしょ?

まとめ

  • move最高、夏休みの宿題いちいちコピーしてられっか。小さめな宿題ならコピーでもいいけども。
  • 友達の宿題を自分のものにするとか最悪だなマジで。

moveなんとなくわかってけどさぁ、どう使えばいいんだよ

STLに入ってるコンテナとかは既にmoveに対応してるみたいですね。

自分のオブジェクトをmoveに対応させたかったらmoveコンストラクタの定義とmove代入演算子オーバーロードが必要。
「はぁ、めんどくせぇなー」って思うかもしれないがC++11ではどうやら何も定義しなければ勝手に以下のものを必要に応じてデフォルトで定義してくれるらしい:

ただここで初見殺しがある。コンストラクタを自分で定義した場合上記のようにコンストラクタをデフォルトで定義されず君のコンストラクタを使うようになる。まあこれは普通だね。
だが、上記のコピーやムーブやデストラクタ関連のいずれかを定義した場合、全てデフォルトで定義されなくなる。

こんな感じ:

// 何も定義していない場合全てデフォルトで定義される
class Hoge
{
    public:
        // コンストラクタとデストラクタ
        // Hoge() = default;  
        // ~Hoge() = default;

        // コピーコンストラクタとコピー代入演算子
        // Hoge(const Hoge&) = default;
        // Hoge& operator=(const Hoge&) = default;

        // ムーブコンストラクタとムーブ代入演算子
        // Hoge(Hoge&&) = default;
        // Hoge& operator=(Hoge&&) = default;
}

// デストラクタを定義してしまった場合
class Piyo
{
    public:
        // コンストラクタとデストラクタ
        // Piyo() = default;  
        ~Piyo(){ ... };

        // コピーやムーブ関連の関数がデフォルトで定義されなくなった
}

ここで = default; というのを使ってるけど、これはどうしてもデフォルトの動作をしてほしいって場合書けばいいというもの。(※コメントは外してね!)なのでこんなことわざわざ覚えてデストラクタ書かないようにしよう!!とか考えなくても= default;ってすればいいのさ。デフォルトの動作と全く同じコードを自前で書いてもいいけどもそういうのは= default;で勝手にやってもらったほうが絶対可読性高い上にミスも減るってばっちゃが言ってた。逆に絶対コピーさせたくねえ!って場合はコピーコンストラクタとコピー代入演算子に対して= delete;ってすればよいのじゃ。

ちょっと待て。ムーブコンストラクタの&&ってなんだよ!!見たこと無いぞ

あーそれね。言い忘れてたよ、それは rvalue reference を受け取りますよ!ってこと。なに、TOEIC満点だけどそんな単語知らないって?やれやれ…
C++で使う変数は色々と分類されるけどすっっごく簡単に大まかにわけるとlvalueとrvalueの2つです。

  • lvalue - int hoge = 42; の hoge の部分のようなオブジェクト。constの場合変更不可の定数になるね
  • rvalue - int hoge = 42; の 42 の部分や関数の返り値のようなそのうち破壊されるような一時オブジェクト。constなrvalueはまず使うことはないだろう

ムーブはこの2つ目のrvalueを利用してるのですごく効率的なのさー。

lvalue か rvalue かによってコピーとムーブのどっちが呼ばれるか決まってくるんだよ~

auto nyan(const Neko& lCat);     // lvalueを処理する
auto nyan(Neko&& rCat);          // rvalueを処理する

...

Neko tama;

nyan(tama);             // tamaはlvalueなのでauto nyan(const Neko& lCat);が呼ばれる。
nyan(std::move(tama));  // std::move(tama)はrvalueなのでauto nyan(Neko&& rCat);が呼ばれる。

なんかようわからんが、std::moveっていう関数使えばmoveしてくれるんか?

違うんだよなぁ…これがまた初見殺し。std::moveは実際にはrvalueへキャストしてるに過ぎない。上のコードのコメントにも書いてあるだろう?「std::move(tama)はrvalue」ってさ。std::moveっていう関数名すごい紛らわしいから本当は別のにしたいけどここまで来ちゃったしstd::moveのままになってしまったらしい。

ちなみにさっきのコード、constだった場合また違った展開になるんだなあこれが

const Neko pochi;

nyan(pochi);            // pochiはlvalue
nyan(std::move(pochi)); // 一応const rvalueではあるが関数宣言がconst lvalueかconstでないrvalueかの2択なのでlvalueのほうの関数を呼び出す

なるほど。ややこしくなってきたからはやいとこまとめてくれ

  • 大きいオブジェクトを関数に渡したりするときはコピーではなくムーブしたほうがパフォーマンス向上が望めるかもしれない
  • 自分のクラスでムーブしたい場合はデフォルトのムーブコンストラクタやムーブ代入演算子を利用するか自分で定義しよう
  • ムーブはrvalueにしか使えない。関数の引数に&&をつければrvalueを受け取れる。
  • STLのコンテナはもうmoveに対応しているし、例えばstd::vectorのemplace_back()関数などにrvalue渡すことができる
  • rvalueに変換したい場合はstd::moveしよう。std::moveは別にムーブしてくれるわけではないぞ~
  • そういえば言ってなかったけど関数から何かを返すとき、RVO (Return Value Optimization) っていうかっこいいやつで最適化してくれるのでそういう時ムーブしてくれるらしい。やったね
  • std::moveのほかにstd::forwardというのがあるが、これは条件付きのstd::moveのようなもので、関数テンプレートをlvalueやrvalueで呼ぶときにrvalueに変換するかしないかを決めるみたいなテンプレートよく使う人には便利な機能みたいです

完全に理解しただなんて大口叩いてすみません本当に、何かおかしな点があったら指摘お願いします>ω<;