こんにちは。田原です。
今回は最後の特殊メンバ関数ムーブ・コンストラクタとその使い方について解説します。また、特殊メンバ関数は常に自動生成されるわけではありません。自動生成されないのはどんな時か、またその理由について説明します。
1.ムーブ・コンストラクタ
前回、前々回と特殊メンバ関数について解説してきましたが、その最後のものがムーブ・コンストラクタです。
まずは前回のサンプルにムーブ・コンストラクタを入れてみます。(解説の都合で少し内容を変えています。)
ムーブ・コンストラクタの構文は自分と同じ型の右辺値参照を1つだけ受け取るコンストラクタです。
内容はムーブ・セマンティクスのお約束に従い「ムーブ」と言える処理にします。
下記の例では、管理しているmFooの所有権とポインタをムーブしています。
class Foo { int mData; public: Foo(int iData) : mData(iData) { } ~Foo() { } }; struct Bar { Foo* mFoo; Bar() : mFoo(nullptr) { } Bar(Foo* iFoo) : mFoo(iFoo) { } ~Bar() { delete mFoo; } // ムーブ・コンストラクタ Bar(Bar&& iRhs) : mFoo(iRhs.mFoo) { iRhs.mFoo = nullptr; } };
ムーブ・コンストラクタの場合、ムーブ代入演算子のように、thisが渡される心配はないので不一致チェックは不要ですし、元のmFooが存在しないのでdelete mFoo;
も不要です。
1-1.ムーブ・コンストラクタの使い所
通常通りクラス型の変数を定義する際に、例えば他の関数の戻り値と同じものをコンストラクトする時が典型的な使い方と思います。
関数の戻り値ですから一時オブジェクト(右辺値)となるため、その文が終わると解放されます。ならば、折角、関数で作られたデータをなるべく有効活用するため、ムーブしたいです。今までのBarクラスを使ったサンプルです。
#include <iostream> class Foo { int mData; public: Foo(int iData) : mData(iData) { std::cout << "Foo::Foo() : this=" << this << "\n"; } ~Foo() { std::cout << "Foo::~Foo() : this=" << this << "\n"; } }; struct Bar { Foo* mFoo; Bar() : mFoo(nullptr) { std::cout << "Bar::Bar()\n"; } Bar(Foo* iFoo) : mFoo(iFoo) { std::cout << "Bar::Bar(Foo*) : mFoo=" << mFoo << "\n"; } ~Bar() { std::cout << "Bar::~Bar() : mFoo=" << mFoo << "\n"; delete mFoo; } // ムーブ・コンストラクタ Bar(Bar&& iRhs) : mFoo(iRhs.mFoo) { std::cout << "Bar::Bar(Foo&&): mFoo=" << mFoo << "\n"; iRhs.mFoo = nullptr; } }; Bar baz() { std::cout << "baz()\n"; Bar ret(new Foo(456)); // Fooコンストラクト・・・① std::cout << "return from baz\n"; return std::move(ret); // 戻り値へムーブ(ムーブ・コンストラクタ) } // retデストラクト(mFooは移動済み) int main() { Bar aBar=baz(); std::cout << "post baz() : aBar.mFoo=" << aBar.mFoo << "\n"; } // aBarデストラクト(mFooは移動済み)
また、関数がクラス・オブジェクトを戻り値とする場合にも使うことができます。
返却するクラス・オブジェクトはreturn文で生成されるため、そのコンストラクト時に用いると効率が上がりそうです。
しかし、実は優秀なコンパイラは戻り値も最適化の対象としており、ムーブよりも更に効率が良い場合が多いです。そのため、優秀なコンパイラ(VC++, gcc, clangは該当します。)を使う時は「成り行き(コンパイラ)」に任せた方が性能が上がります。
詳しいことは次回解説します。キーワードはRVO(Return Value Optimization)、NRVO(Named Return Value Optimization)です。
2.ところで右辺値参照は左辺値です
ここが個人的に一番苦しみました。右辺値参照は「右辺値」を参照しているので右辺値のように感じ取れますし、字面も右辺値っぽいですね。しかし、「左辺値」です。
以前解説したように右辺値は値です。値は容器の中身ですね。
しかし、コンピュータの実装上例えば1+1を計算した結果をどこかに記録しています。それが保持されている容器が一時オブジェクトです。一時オブジェクトは一時的な領域で直ぐに破棄されますが、メモリ領域なのでアドレスを持つ「容器」です。(厳密にはちょっと違いますが、ややこしい話は割愛します。)
その一時オブジェクト(メモリ領域)を参照している右辺値参照は左辺値(容器)なのです。そもそも、右辺値参照は参照先のデータを空へ変更することが目的ですから、当然その中身を変更できないといけません。つまり、右辺値参照が左辺値であることは当然のことなのです。
従って、右辺値参照(左辺値)を更に右辺値参照で受け取りたい場合は、std::moveして右辺値へ偽装する必要があります。
そのサンプルを「ムーブ・コンストラクタの使用例」に追加します。
void qux(Bar&& iBar) { std::cout << "qux() : iBar.mFoo=" << iBar.mFoo << "\n"; #if 1 Bar aBar(std::move(iBar)); // aBarへムーブ(ムーブ・コンストラクタ) #else Bar aBar(iBar); // aBarへコピーしようとしてもコピー・コンストラクタはないのでエラー #endif std::cout << "qux() : aBar.mFoo=" << aBar.mFoo << "\n"; } // aBarデストラクト(ここで①のFooデストラクト)
quxは、右辺値参照iBarで引数を受け取ります。右辺値参照なので他の関数の戻り値等の右辺値を受け取ることができます。
しかし、一度受け取ってしまったiBarは上述の通り左辺値となります。
これを再度Barのムーブ・コンストラクタへ引き渡したい場合は、再度右辺値を偽装するためにstd::move必要があります。
Wandboxで試してみる #if 1を#if 0へ変更してみて下さい。
3.特殊メンバ関数が自動生成されない時
3-1.自動生成ルール
2.のサンプルで#if 0にした時、コピー・コンストラクタがないのでコピーできないと書きました。確かにコピー・コンストラクタを書いてはいないのですが、コピー・コンストラクタは自動生成される筈ですね。
そう、実は特殊メンバ関数は常に自動生成されるわけではありません。
例えば、第34回で解説したように所有権を管理しているようなオブジェクトをコピーするとデストラクト時に異常が発生します。自動生成される特殊メンバ関数でこのようなことが普通に起きるとあまりうれしくないですね。ですので、このような問題が起きにくいように、特定のケースでは自動生成されません。
ルールとしては、以下の通りです。
- ユーザがデストラクタを定義した場合
ムーブ特殊メンバ関数(ムーブ・コンストラクタとムーブ代入演算子)は自動生成されません。
コピー特殊メンバ関数(コピー・コンストラクタとコピー代入演算子)は自動生成されますが、非推奨。(将来自動生成されなくなる予定)
デストラクタが定義されているということは、デストラクタで何らかのリソースを解放する場合が多いです。その時、そのクラスが管理するリソースを自動生成されるムーブ特殊メンバ関数で適切にムーブできない可能性が高いためです。
コピー特殊メンバ関数も同様なのですが、C++11より前は自動生成されていたので互換性のため自動生成されます。 -
ユーザが1つでもコピー特殊メンバ関数を定義した場合
コピー/ムーブ特殊メンバ関数は自動生成されません。
コピーとムーブの両方に対応するクラスは特殊なので、自動生成で賄えない可能性が高いためと思われます。 -
ユーザが1つでもムーブ特殊メンバ関数を定義した場合
コピー/ムーブ特殊メンバ関数は自動生成されません。
コピーとムーブの両方に対応するクラスは特殊なので、自動生成で賄えない可能性が高いためと思われます。 -
ユーザがデフォルト以外のコンストラクタを定義した場合
デフォルト・コンストラクタは自動生成されません。(C++11以前より)
3-2.自動生成される特殊メンバ関数を自動生成させたくない時
例えば、シングルトンと呼ばれる良く使われるクラスのパターンがあります。
シングルトンは、オブジェクトを1つだけしか生成できないことを保証するクラスです。
それがコピーできてしまうとオブジェクトが複数存在します。ムーブも同様です。ムーブにより管理している領域を移動しますが、クラス・オブジェクト自体はコピーされるので複数のオブジェクトを生成できます。
このような時は、自動生成されては困る特殊メンバ関数にdelete指定します。(下記の例はシングルトンではありません。)
class Foo { int mData; public: Foo(int iData) : mData(iData) { } Foo(Foo const&) = delete; Foo& Operator=(Foo const&) = delete; Foo(Foo&&) = delete; Foo& Operator=(Foo&&) = delete; };
3-3.自動生成されなかった特殊メンバ関数と同じ関数を生成する方法
例えば、ユーザが空のデストラクタを書くことが有ります。第27回目で解説したように動的ポリモーフィズムを使う場合には仮想デストラクタを定義するべきです。その際デストラクタで処理したいことがないことも少なくありません。そのような時は空の仮想デストラクタを定義するでしょう。
このケースでは、コピー特殊メンバ関数やムーブ特殊メンバ関数は自動生成されるもので良い場合も少なくありません。
このような時は、default指定して自動生成されるものと同じ特殊メンバ関数を生成させることができます。
(因みに、特殊メンバ関数の自動生成ルールを判定する際、default指定して強制的に自動生成させた場合も「ユーザが定義した」ものとして扱われます。)
少し無理矢理なサンプルですが、Handleクラスを作ってみました。
これはハンドル値を管理するクラスで、ハンドル値には無効値(kInvalidHandle)を定義しています。
実際に使う場面はあまりないような気もしますが、コピー・コンストラクタとムーブ・コンストラクタの両方を定義しています。
ムーブ・コンストラクタを定義するとコピー・コンストラクタは自動生成されなくなるので、default指定しています。
#include <iostream> #include <limits> #include <string> class Handle { unsigned mValue; public: const static unsigned kInvalid = std::numeric_limits<unsigned>::max(); std::string str() { return (mValue == kInvalid)?std::string("[Invalied]"):std::to_string(mValue); } Handle() : mValue(kInvalid) { } Handle(unsigned iValue) : mValue(iValue) { } Handle(Handle const&) = default; Handle(Handle&& iRhs) : mValue(iRhs.mValue) { iRhs.mValue = kInvalid; } };
このクラスを基底クラスで管理し、動的ポリモーフィズムで使う場合を想定してみました。
(派生クラスDerivedは手抜きでごめんなさい。)
class Base { Handle mHandle; public: std::string str() { return mHandle.str(); } Base(unsigned iValue) : mHandle(iValue) { } Base(Base&&) = default; virtual ~Base() { } }; class Derived : public Base { }; int main() { Base aBase(123); Base aBase2=std::move(aBase); std::cout << "aBase =" << aBase.str() << "\n"; std::cout << "aBase2=" << aBase2.str() << "\n"; }
Baseクラスに自動生成と同じムーブ・コンストラクタ有り
↓
Base aBase2=std::move(aBase);の行でaBase2を生成するためにそのムーブ・コンストラクタが呼ばれる
↓
これはメンバ変数のムーブ・コンストラクタを呼び出すのでHandle(Handle&& iRhs)が呼ばれ、メンバ変数mValueコピー後ムーブ元のaBaseのmHandle.mValueにkInvalidが設定される。
次に、Wandboxのソース31行目にあるBaseのムーブ・コンストラクタをコメントアウトしてみて下さい。
Baseにムーブ・コンストラクタ無し → コピー・コンストラクタが自動生成される
↓
Base aBase2=std::move(aBase);の行でaBase2を生成するためにそのムーブ・コンストラクタが呼ばれるが無いのでコピー・コンストラクタが呼ばれる
↓
これはメンバ変数のコピー・コンストラクタを呼び出すのでHandle(Handle const& iRhs)が呼ばれる。これは自動生成と同じものなのでメンバ変数を単純にコピーする。
のように動作します。
4.まとめ
今回はムーブ・コンストラクタ、特殊メンバ関数が自動生成されない時、特殊メンバ関数の働き方について解説してみました。
特に自動生成とdelete/defaultの動きについては分かりにくいのでサンプルを使って解説してみました。少しでも理解の助けになれば幸いです。
次回は、今回解説できなかったムーブや特殊メンバ関数周りの話題を解説します。お楽しみに。