インスタンスを返す関数における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;
}