インスタンスを返す関数におけるmoveの注意点
こんにちはtatsyです。
最近Effective Modern C++を読んでいたのですが、読んでいて気づいた、言われれば当たり前だけど、意外とやってしまうmoveの注意点についてご紹介したいと思います。
Effective Modern C++ (外部ページに飛びます)
おそらく、moveの最もよく使われるであろう使用場面はfactoryメソッドなどにおいてインスタンスをreturnするときだと思います。
例えば、次のように何らかのサイズを引数に取るコンストラクタをmakeInstance(int)というfactoryメソッドで呼び出す場合を考えます。
Foo makeInstance(int size) {
Foo foo(size);
return std::move(foo);
}
このソースコードは正しいソースコードで、クラスFooでMoverが定義されていれば、適切に内容がmoveされます。
では、次の例ではどうでしょうか?
Foo makeInstance(int size) {
const Foo foo(size);
return std::move(foo);
}
違いは、ローカルのfooがconstになったことです。関数内で変更されない値にconstをつける(多くの場合は良い)癖のついている人は、このように書いてしまうことがあるかもしれません。
が、このコードは間違いです。ご存知の通り、Moverは通常constでない右辺値参照を引数に取ります。ですから、上記の場合は、std::moveにより、returnされる時点での型はconst Foo&&です。
これが呼び出し元の値に代入される時にはconstが付いているためにFoo(Foo&&)やoperator=(Foo&&)とマッチせず、その結果としてコピーコンストラクタや代入の演算子が呼び出されることになります。
これはなんともやりがちなミスですね。多分、自分のコードでも結構やってしまっていたと思うので、再度コードを見直して見たいと思います。
自分がテストのために書いたコードを一応下に貼り付けて置きます。
最後までお読み頂きありがとうございました!
#include <cstring>
class Foo {
private:
int size;
int* data;
public:
Foo()
: size(0)
, data(nullptr) {
}
Foo(int sz)
: size(sz)
, data(new int[sz]) {
}
Foo(const Foo& foo)
: size(foo.size)
, data(new int[foo.size]) {
memcpy(this->data, foo.data, sizeof(int) * foo.size);
std::cout << "Copy" << std::endl;
}
Foo(Foo&& foo)
: size(foo.size)
, data(foo.data) {
foo.size = 0;
foo.data = nullptr;
std::cout << "Move" << std::endl;
}
~Foo() {
delete[] data;
}
Foo& operator=(const Foo& foo) {
delete[] this->data;
this->size = foo.size;
this->data = new int[foo.size];
memcpy(this->data, foo.data, sizeof(int) * foo.size);
return *this;
}
Foo& operator=(Foo&& foo) {
delete[] this->data;
this->size = foo.size;
this->data = foo.data;
foo.size = 0;
foo.data = nullptr;
return *this;
}
static Foo makeInstance(int sz) {
const Foo foo(sz);
return std::move(foo);
}
inline int length() const { return size; }
};
int main(int argc, char** argv) {
Foo foo = Foo::makeInstance(10);
std::cout << foo.length() << std::endl;
}