こちらはC++ Advent Calendar 2019の15日目になります。
昨日は @tk-aria さんでした。ありがとうございました。
明日は nullptr さんです。よろしくお願いいたします。
はじめに
今回の内容は初学者向けであり
右辺値参照やムーブセマンティクスの説明ではありません。
なぜムーブ(move)を使用するほうがいいのかの話をします。
ムーブとは何ですかって聞かれたときに右辺値・左辺値とかの話から始めると片手間では教える側も教わる側も辛いので、その辺りの話はあえて避けています。
仕事とかでC++を使っていると、「そこはムーブにしたほうがいいと思います」
となる機会が良くあります。
しかし、C++を書く人が皆ムーブを使いこなせているはずもなく
訳のわからない std::move
とか使ってみるより、コピーをしておけば間違いがないのだ。不安もないのだ。
なんだよ、ムーブってC++難しすぎるんだよ
なぜムーブにする必要があるのか
そもそもムーブにしなくても良いです。コピーで良いです。
※ std::unique_ptr
などコピーができないものを扱っているときは必要になるかもしれない
つまり基本的には、ムーブすることはmustではない。
したほうがいいだけで、しなくてもよい。
なぜムーブしたほうがいいか
そのほうが実行速度が速くなるからである。
ではなぜ速くなるのか
ムーブはコピーではなくてムーブ
まず、最初にムーブはコピーではなくムーブです。(頭悪そう)
なので基本的にそれ以降は使用しない変数に対して行います。
std::vector v1{1,2,3};
std::vector v2 = v1; // これはコピー
// v1はまだ使用できる
std::vector v3 = std::move(v1); // これはムーブ
// 以降で v1 は使用しない
※ちなみにムーブ後も使用できないわけではないが実装を理解してムーブ後の状態は理解しておく必要はあると思います。(基本しない)
ムーブはシャローコピーをするから速い
少し話がずれますが
単にコピーと言ってもディープコピーとシャローコピーがありあす。
ディープコピーはポインタの参照先の実体もコピーするのに対してシャローコピーはポインタ変数をコピーするだけなので早い。
C++のSTLとかは基本コピーはディープコピーされるように作られています。
なので大きなサイズのvectorとかをコピーすればその分コストがかかるわけです。
// サイズの大きなクラス
class LargeClass
{
// なんかいろいろある
};
// ディープコピー
class DeepCopy
{
LargeClass* p;
public:
DeepCopy()
{
p = new LargeClass();
}
~DeepCopy()
{
delete p;
}
DeepCopy(const DeepCopy& other)
{
// メモリ領域確保
p = new LargeClass();
// 実体のコピー
*p = *other.p;
}
};
// シャローコピー
class ShallowCopy
{
LargeClass* p;
public:
ShallowCopy()
{
p = new LargeClass();
}
~ShallowCopy()
{
delete p;
}
ShallowCopy(const ShallowCopy& other)
{
// ポインタだけコピー
p = other.p;
}
};
ディープコピーとシャローコピーの違いがわかり、シャローコピーのほうが速いことがなんとなくわかったかもしれませんが
C++の場合は上記のようなシャローコピーをすると大変まずいことになります。
実際に実行してみるとなにやら怖いことになります。
https://wandbox.org/permlink/IBfQYFrceqNiANL8
int main()
{
ShallowCopy s1;
ShallowCopy s2 = s1;
}
何が起きたかというと、デストラクタでdeleteしてるからポインタを単にコピーしちゃうと同じメモリが解放されてしまって
2重解放になってしまいバグってしまったということです。
*** Error in `./prog.exe': double free or corruption (fasttop): 0x0000000001148a00 ***
こういったことを回避する方法としてスマートポインタ(std::shared_ptr
)を使うのも一つの方法です。
が、そもそもコピーである必要がない場合は考えられないでしょうか?
コピーした後にその変数を使用しない場合、ポインタをnullptrにでもしておけば
メモリの二重解放は回避できます。
int main()
{
ShallowCopy s1;
ShallowCopy s2 = s1;
s1.p = nullptr; // もう使用しないからnullptrでもいれとく
}
これが、ムーブなんや
ムーブは基本シャローコピーと二重解放回避の後始末という一連の流れである
なので(ディープ)コピーするより速くなる。
class Hoge
{
LargeClass* p;
public:
Hoge()
{
p = new LargeClass();
}
~Hoge()
{
delete p;
}
// コピーコンストラクタ
Hoge(const Hoge& other)
{
// メモリ領域確保
p = new LargeClass();
// 実体のコピー
*p = *other.p;
}
// ムーブコンストラクタ
Hoge(Hoge&& other) noexcept
{
// ポインタだけコピー
p = other.p;
// 後始末
other.p = nullptr;
}
};
int main()
{
Hoge h1;
Hoge h2 = h1; // コピー
Hoge h3 = std::move(h1); // ムーブ
}
ムーブは後始末があるのでconstをつけれない。
constをつけた変数をmoveしてもコピーコンストラクタが呼ばれている可能性があるので注意してほしい。
int main()
{
const Hoge h1;
Hoge h2 = h1; // コピー
Hoge h3 = std::move(h1); // コピー
}
まとめ
- 基本ディープコピーよりシャローコピーのほうが速い
- ムーブはシャローコピーとメモリ二重解放回避の後始末をしている
- なので、基本コピーよりムーブのほうが速い
- ムーブはそれ以降使用しない変数にする
※なお、ポインタが絡まないようなclassはディープもシャローもくそもないのでムーブのしようがない。
さいごに
だから我々はmoveする
我々と言いましたが自分なりの見解なので間違っているところなど指摘があればよろしくお願いします。
また、右辺値参照やムーブセマンティクスについてもっと詳しく知りたい方は以下などを参考にしてください。(調べればいくらでも出てくる)