ユーザー定義のクラスにmove constructorを追加するための方法についてまとめます。
既存の型にstd::move
使うと便利なので、自作の型に対しても定義したい、という方向けです。
先に結論をまとめておくと、注意するべきは以下の3点です:
- ユーザー定義のコピーコンストラクタやデストラクタがあるとデフォルトのムーブコンストラクタは作られない
- ムーブされた残り滓
a = std::move(b)
のb
のデストラクタは呼ばれる - noexcept付けないとムーブの恩恵が得られなくなる場合がある
ムーブコンストラクタはいつ勝手に作られるの?
unique_ptr
やvector
の様な既存の型を複数個組合せた構造体
struct MyStruct{
std::unique_ptr<MyStruct> p;
std::vector<int> v;
};
において期待されるムーブコンストラクタは、
個々の要素p
, v
のムーブコンストラクタをそれぞれ呼び出すようなものでしょう。
以下の5つの条件を満す場合、デフォルトのムーブコンストラクタがimplicitに定義されます。
- ユーザー定義コピーコンストラクタが無い
- ユーザー定義コピー代入演算子が無い
- ユーザー定義ムーブ代入演算子が無い
- ユーザー定義デストラクタがない
- (until C++14) 以下に説明する条件によってムーブコンストラクタがdeleted指定になっていない
ただしT(T&&) = default;
で強制的にデフォルトのムーブコンストラクタを定義させる事ができます。
struct A{
A(){}
~A(){} // 自作デストラクタがあるので
// デフォルトのムーブコンストラクタの生成が阻害される
A(A&&) = default; // これでデフォルトのムーブコンストラクタが定義される
};
以下の場合にはデフォルトのムーブコンストラクタがdeleted指定されます
- ムーブできない非静的データメンバーがある
- ムーブできないクラスのインスタンスを含んでいる
- デストラクタが無いかアクセスできないクラスのインスタンスを含んでいる
- 共用体であり、非自明なコピーコンストラクタを持つvariant memberを持つ場合
- (until C++14) ムーブコンストラクタが無く、自明なコピーができない(not trivially copyable)非静的メンバーがある
参考HP
ムーブされた残骸に対しても呼ばれるデストラクタ
ムーブされた残り滓a = std::move(b)
のb
のデストラクタは呼ばれる:
#include <iostream>
class A {
public:
A() { std::cout << "Constructor " << std::endl; }
A(const A &a) { std::cout << "Copy Constructor " << std::endl; }
A(A &&a) noexcept { std::cout << "Move Constructor " << std::endl; }
~A() { std::cout << "Destructor " << std::endl; }
};
A create_A() {
A tmp; // call constructor
return std::move(tmp); // call move constructor
// call destructor
}
int main() {
A a = create_A();
return 0;
// call destructor
}
この出力は以下のようにデストラクタが2回呼ばれている事に注意する:
Constructor
Move Constructor
Destructor
Destructor
付けられる時はnoexceptを必ず付けよう
ムーブコンストラクタがnoexceptであるかを判定しているアルゴリズムがあります。
例えばstd::vector
のpush_back
ではGCC4.7以降でstd::move_if_noexcept
を使用するため、
noexcept
のついていないムーブコンストラクタは使われません。
自作クラスのムーブコンストラクタを書く際は注意しましょう。
参考HP
- ムーブコンストラクタにnoexceptを付けないと呼ばれないことがある (Faith and Brave - C++で遊ぼう)
- 12.5.4 Declare noexcept the move constructor and move assignment operator (High Integrity C++ Coding Standard)
move assignment operator (移動代入演算子)
効率的な実装の方法として以下の方法が考えられます:
- 先にmove assignment operator
operator=(T&&)
を定義する - move constructorを
T(T&& other) noexcept { *this = std::move(other); }
で定義する
こうすれば同じ処理を重複なく書けます。
こちらにもnoexcept
をつける事を忘れずに。また自己代入に注意しましょう。
T& operator=(T&& other) noexcept{
if(this != &other){
// moveの処理
}
return *this;
}