#本記事の目的
何々の時に〇〇コンストラクタ、〇〇代入演算子が呼び出されるというのがなかなか覚えられないので(初心者乙)、練習がてら整理してみた記事です。色々やってみた系の記事なので、実用性の無いコードもありますがご承知おきください。
#実験用自作クラスXの定義
まず、(デフォルト・コピー・ムーブ)コンストラクタ、デストラクタ、オーバーライドされたコピー代入演算子、ムーブ代入演算子にデバッグ文を仕込んだクラスX
をヘッダファイルX.h
に定義します。また、アドレスもデバッグ出力することで、生成されたオブジェクトや引数で渡されたオブジェクトを一意に識別します。
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#include <iostream>
#include <memory>
class X {
public:
X() {
std::cout << this << " : constructor." << std::endl;
}
~X() {
std::cout << this << " : destructor." << std::endl;
}
X(const X& x) {
std::cout << this << " <-- " << std::addressof(x) << " : copy constructor." << std::endl;
}
X(X&& x) {
std::cout << this << " <-- " << std::addressof(x) << " : move constructor." << std::endl;
}
X& operator=(const X& x) {
std::cout << this << " <-- " << std::addressof(x) << " : copy assignment operator." << std::endl;
return *this;
}
X& operator=(X&& x) {
std::cout << this << " <-- " << std::addressof(x) << " : move assignment operator." << std::endl;
return *this;
}
};
本記事ではこの自作クラスX
を使いまわして、あーだこーだ言います。
#即席value category講座
突然ですが…
本記事中に「lvalue式」「xvalue式」「prvalue式」という用語が出てきます。「わーいvalue category!あかりvalue categoryだーいすき!」とテンションが上がる人も中にはいると思いますが、C++初学者の人(本当の意味の初学者)がvalue categoryに首を突っ込む機会はあまりないと思うので、ここで「本記事を読むうえで最低限必要となる」レベルで簡単に説明します。
##value categoryとは
まず、value categoryとは何か。これは式の分類です。具体的には「lvalue」「xvalue」「prvalue」「glvalue」「rvalue」の総称です。「int型」「double型」「関数型」などの総称が「型」であるのと同様。名前に「value」が含まれてこそいますが、あくまで式の分類です。
##式(expression)とは
式とはざっくりいうと「変数、関数、定数、文字列リテラル。または式と演算子の特定の規則に従った並び」です。「n=42
」「a+i
」「a[0]
」「a*b+c*d
」「"Hello, world!"
」「printf
」などが式です。関数呼出し式「printf("Hello, world!\n")
」も式です。ざっくりなのでsizeof(型名)
みたいなのが抜けますね。でも言いたいことは伝わると思います。
##lvalue式、xvalue式、rvalue式とは
任意の式は「lvalue式」「xvalue式」「prvalue式」のいずれか1つに分類されます。ある条件を満たすような式は「lvalue式」に分類され、また別のある条件を満たすような式は「prvalue式」に分類される、といった具合です。これらは排他的な分類であり、「lvalue式であると同時にprvalue式であるような式」というものは存在しません。逆に、どれにも当てはまらない式というのも存在しません。
式は「lvalue式」「xvalue式」「prvalue式」のいずれか1つに分類される、それは分かった。では、その「lvalue式」「xvalue式」「prvalue式」というのは一体何やねん?といきたいところですが、それらの定義について真面目に考え始めるとC++の深淵に飲み込まれてしまいますので、ここでは本記事のコード例に出てくる式のみについて、そのvalue categoryを示しておきます。とりあえず丸暗記でいいです。
式 | value category | つまり |
---|---|---|
x | lvalue式 | 変数 |
X() | prvalue式 | コンストラクタ呼出し式 |
move(x) | xvalue式 | move関数呼出し式 |
関数の仮引数 | lvalue式 | 仮引数 |
##関数呼出し式のvalue category
関数呼出し式については、関数の戻り値型によってvalue categoryが異なります。
関数の戻り値型 | 関数呼出し式のvalue category |
---|---|
reference以外の型 | prvalue式 |
型lvalue reference | lvalue式 |
型rvalue reference | xvalue式 |
以上は、少なくともこの記事を読むうえでは丸暗記で問題なく、意味を考えて苦しむ必要はありません。
##glvalue式、rvalue式とは
glvalue式とは、lvalue式とxvalue式の総称です。rvalue式とは、prvalue式とxvalue式の総称です。
なぜそんな総称が必要なのか?例えばlvalue式とxvalue式の両方に当てはまる性質について述べたいとき「lvalue式およびxvalue式は〇〇の性質をもつ」と記述すると長いので「glvalue式は~」と手短に言うためです。有理数と無理数の総称が「実数」であるようなものですね。規格票で多用される言葉なので、規格票を読む予定があるなら覚えておいても損はないでしょう(という程度のものです)。
「即席value category講座」は、ここまでで終わりです。
##もっと詳しく
value categoryについて、もっと真面目な情報が知りたい場合、cppreference.comの「Value categories」がおすすめです。毎日目を通していたら、ある日突然、value categoryの本質的意味が腑に落ちる時がくるかもしれません。
長くなりましたが、ここからが本題です。
#一時オブジェクトの寿命
式X()
(prvalue式)を評価した結果、一時オブジェクトが生成され、式文の完全式の終わりで消滅します。そのため、生成された一時オブジェクトの寿命はブロックの終わりに到達しません。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X();
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffd4747496f : constructor.
0x7ffd4747496f : destructor.
******** 壁の中 ********
******** 壁の中 ********
#一時オブジェクトをreferenceで束縛する
一時オブジェクトをrvalue reference型変数で束縛すると、一時オブジェクトの寿命はreferenceの寿命(つまりブロックの終わり)まで延長されます。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X&& r = X();
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffd621bf9c7 : constructor.
******** 壁の中 ********
0x7ffd621bf9c7 : destructor.
******** 壁の中 ********
※「一時オブジェクトの寿命」「一時オブジェクトをreferenceで束縛する」の内容は、こちらの記事を参考にさせていただきました。
「一時オブジェクトの寿命と右辺値参照、ムーブセマンティクスのお話」
#コンストラクタ
##コピーコンストラクタ
変数b
が指し示すオブジェクトの構築時、初期化元にlvalue式(a
)を指定すると、b
のコピーコンストラクタが呼び出されます。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X a;
X b = a; // OK. bのコピーコンストラクタが呼び出しされる。
// X b{a}; // OK. bのコピーコンストラクタが呼び出しされる。
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffe812546ef : constructor.
0x7ffe812546ee <-- 0x7ffe812546ef : copy constructor.
******** 壁の中 ********
0x7ffe812546ee : destructor.
0x7ffe812546ef : destructor.
******** 壁の中 ********
##ムーブコンストラクタ
変数b
の構築時、初期化元がxvalue式の場合、初期化先(b
)のムーブコンストラクタが呼び出されます。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X a;
X b = move(a); // OK. bのムーブコンストラクタが呼び出される。
// X b{move(a)}; // OK. bのムーブコンストラクタが呼び出される。
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffccd7716cf : constructor.
0x7ffccd7716ce <-- 0x7ffccd7716cf : move constructor.
******** 壁の中 ********
0x7ffccd7716ce : destructor.
0x7ffccd7716cf : destructor.
******** 壁の中 ********
初期化元がprvalue式である場合として、以下のコードを書いてみましたが、コード例がたまたまRVO(Return Value Optimization)の適用要件を満たしたのか、ムーブコンストラクタは呼び出されませんでした。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X x = X();
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7fff1ba02ccf : constructor.
******** 壁の中 ********
0x7fff1ba02ccf : destructor.
******** 壁の中 ********
#代入演算子
##コピー代入演算子
代入演算子の右オペランド(a
)がlvalue式の場合、左オペランド(b
)のコピー代入演算子が呼び出されます。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X a;
X b;
b = a;
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffd690aedef : constructor.
0x7ffd690aedee : constructor.
0x7ffd690aedee <-- 0x7ffd690aedef : copy assignment operator.
******** 壁の中 ********
0x7ffd690aedee : destructor.
0x7ffd690aedef : destructor.
******** 壁の中 ********
##ムーブ代入演算子
代入演算子の右オペランドがprvalue式の場合、左オペランド(x
)のムーブ代入演算子が呼び出されます。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X x;
x = X();
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffc357f933e : constructor.
0x7ffc357f933f : constructor.
0x7ffc357f933e <-- 0x7ffc357f933f : move assignment operator.
0x7ffc357f933f : destructor.
******** 壁の中 ********
0x7ffc357f933e : destructor.
******** 壁の中 ********
代入演算子の右オペランド(a
)がxvalue式の場合、代入先(b
)のムーブ代入演算子が呼び出しされます。
#include <iostream>
#include "X.h"
using namespace std;
int main()
{
{
X a;
X b;
b = move(a);
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffd7e0598ef : constructor.
0x7ffd7e0598ee : constructor.
0x7ffd7e0598ee <-- 0x7ffd7e0598ef : move assignment operator.
******** 壁の中 ********
0x7ffd7e0598ee : destructor.
0x7ffd7e0598ef : destructor.
******** 壁の中 ********
#関数の引数
##関数(仮引数は型X)呼出し式の実引数にlvalue式を指定した場合
実引数にlvalue式を指定して関数(仮引数の型は型X
)を呼出した場合、仮引数のコピーコンストラクタが呼出しされます。
#include <iostream>
#include "X.h"
using namespace std;
void f(X x){ }
int main()
{
{
X x;
f(x);
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7fff7be9e60e : constructor.
0x7fff7be9e60f <-- 0x7fff7be9e60e : copy constructor.
0x7fff7be9e60f : destructor.
******** 壁の中 ********
0x7fff7be9e60e : destructor.
******** 壁の中 ********
##関数(仮引数の型X)呼出し式の実引数にprvalue式を指定した場合
実引数にprvalue式を指定して関数(仮引数の型は型(コメント欄でのakinomyogaさんのご指摘を受けて、以下の文に修正しました。コード例も修正。)X
)を呼出した場合、仮引数のムーブコンストラクタが呼出しされます。
(2017.6.18修正後)実引数にprvalue式を指定して関数(仮引数の型は型X
)を呼び出した場合として以下のコードを書いてみましたが、こちらもコード例がたまたまcopy elisionの適用要件を満たしたのか、仮引数のムーブコンストラクタは呼び出されませんでした。
#include <iostream>
#include <memory>
#include "X.h"
using namespace std;
// X f(X x){ return x; } // 2017.6.18削除
void f(X x){} // 2017.6.18追記
int main(void)
{
{
f(X());
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffecbac34cc : constructor.
0x7ffecbac34cc : destructor.
******** 壁の中 ********
******** 壁の中 ********
##関数(仮引数の型X)呼出し式の実引数にxvalue式を指定した場合
#include <iostream>
#include "X.h"
using namespace std;
void f(X x){}
int main()
{
{
X x;
f(move(x));
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffd0efaac0e : constructor.
0x7ffd0efaac0f <-- 0x7ffd0efaac0e : move constructor.
0x7ffd0efaac0f : destructor.
******** 壁の中 ********
0x7ffd0efaac0e : destructor.
******** 壁の中 ********
##関数(仮引数の型X&)呼出し式の実引数にlvalue式を指定した場合
仮引数の型が型X&
をもつため、当然ですが仮引数のコンストラクタは呼出しされません。
#include <iostream>
#include "X.h"
using namespace std;
void f(X& x){}
int main()
{
{
X x;
f(x);
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffff4f324af : constructor.
******** 壁の中 ********
0x7ffff4f324af : destructor.
******** 壁の中 ********
##関数(仮引数の型X&&)呼出し式の実引数にprvalue式を指定した場合
仮引数がX&&
の場合も当然ですが、コンストラクタは呼出しされません。
#include <iostream>
#include "X.h"
using namespace std;
void f(X&& x){}
int main()
{
{
f(X());
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffcebbffe3f : constructor.
0x7ffcebbffe3f : destructor.
******** 壁の中 ********
******** 壁の中 ********
##関数(仮引数の型X&&)呼出し式の実引数にxvalue式を指定した場合
こちらも当然ですが、仮引数のコンストラクタは呼出しされません。
#include <iostream>
#include "X.h"
using namespace std;
void f(X&& x){}
int main()
{
{
X x;
f(move(x));
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7fff8f5bf97f : constructor.
******** 壁の中 ********
0x7fff8f5bf97f : destructor.
******** 壁の中 ********
#関数の戻り値
##関数の戻り値型がreference以外の場合
関数の戻り値型がreference以外の場合、関数呼出し式はprvalue式となります。
#include <iostream>
#include <memory>
#include "X.h"
using namespace std;
X f() { return X(); }
int main()
{
{
X x;
x = f();
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x7ffdfb25b70e : constructor.
0x7ffdfb25b70f : constructor.
0x7ffdfb25b70e <-- 0x7ffdfb25b70f : move assignment operator.
0x7ffdfb25b70f : destructor.
******** 壁の中 ********
0x7ffdfb25b70e : destructor.
******** 壁の中 ********
式f()
はprvalue式であり、その結果が一時オブジェクトを生成し、それを呼出し元の変数に代入します。代入式の右オペランド(f()
)がprvalue式であるため、代入先(x
)のムーブ代入演算子が呼び出されます。
##関数の戻り値型が型lvalue referenceの場合
戻り値型がlvalue reference型の関数の呼出し式は、lvalue式となりますので、コピー代入演算子が呼び出しされます。
#include <iostream>
#include <memory>
#include "X.h"
using namespace std;
X gx;
X& f() { return gx; }
int main()
{
{
X x;
x = f();
cout << "******** 壁の中 ********" << endl;
} // (´・ω・`)
cout << "******** 壁の中 ********" << endl;
}
0x6018f1 : constructor.
0x7ffe3b0cfc5f : constructor.
0x7ffe3b0cfc5f <-- 0x6018f1 : copy assignment operator.
******** 壁の中 ********
0x7ffe3b0cfc5f : destructor.
******** 壁の中 ********
0x6018f1 : destructor.
##関数の戻り値型が型rvalue referenceの場合
「ムーブコンストラクタ」「ムーブ代入演算子」を参照。
#まとめ
##初期化指定のある変数(reference以外の型をもつ)の宣言時
- 初期化元がlvalue式の場合、コピーコンストラクタが呼出しされます。
- 初期化元がrvalue式またはxvalue式の場合、ムーブコンストラクタが呼出しされます。
##代入式の評価時
- 右オペランドがlvalue式の場合、コピー代入演算子が呼出しされます。
- 右オペランドがrvalue式またはxvalue式の場合、ムーブ代入演算子が呼出しされます。
##関数の戻り値型について
関数の戻り値型と、関数呼出し式のvalue categoryをまとめると以下となります。
関数の戻り値型 | 関数呼出し式のvalue category |
---|---|
reference以外の型 | prvalue式 |
型lvalue reference | lvalue式 |
型rvalue reference | xvalue式 |
関数呼出し式の結果を代入する場合、関数呼出し式のvalue category(上の表を参照)におうじて、対応するオーバーロードされた代入演算子が呼出しされます。上の表に、呼出しされる代入演算子を追加したものが以下となります。
戻り値型 | 関数呼出し式のvalue category | 関数呼出しの結果を代入時、呼出しされる代入先のメンバ関数 |
---|---|---|
型reference以外 | prvalue式 | ムーブ代入演算子 |
型lvalue reference | lvalue式 | コピー代入演算子 |
型rvalue reference | xvalue式 | ムーブ代入演算子 |
例えば、b = std::move(x)
という式は、関数std::move()
の戻り値型が型「rvalue reference」をもつため、関数呼出し式std::move(x)
のvalue categoryはxvalueとなります。つまり、代入式の代入元がxvalueとなるため、代入先のムーブ代入演算子が呼び出しされます。
##関数呼出し式の実引数と仮引数について
関数呼出し式の実引数のvalue categoryと、仮引数の型に応じて呼び出される仮引数のコンストラクタは以下となります。
実引数の式の value category |
仮引数が 型reference以外 |
仮引数が 型lvalue reference |
仮引数が 型rvalue reference |
---|---|---|---|
lvalue式 | コピー コンストラクタ |
(束縛のみ) | (ill-formed) |
prvalue式 xvalue式 |
ムーブ コンストラクタ |
(ill-formed) | (束縛のみ) |
※「束縛のみ」とは、仮引数から実引数の式が指すオブジェクトを束縛するのみで、仮引数のコンストラクタは呼出しされない、という事を示します。
※(2017.6.18追記)実引数の式がprvalue式であり、仮引数の型がreference以外の場合のムーブコンストラクタは、copy elisionの適用により省略される場合があります。
つまり…
- 仮引数の型がreferenceの場合、仮引数から(実引数の式が指し示す)オブジェクトへの束縛が行われるだけで、オブジェクトの構築は必要ないため、仮引数のコンストラクターは呼出しされません。
- 仮引数の型がreference以外の場合、仮引数が束縛する独自のオブジェクトを構築する必要があるため、(コピーまたはムーブ)コンストラクタが呼出しされます(仮引数は普通にlvalueですからね)。そして、コピーコンストラクタ、ムーブコンストラクタのどちらが呼び出されるかは、実引数の式のvalue categoryに依存します。
原理原則を考えれば当たり前のことですが、まあ、言われてみれば確かにそうだよな…と思いました。
#おわりに
くぅ疲です。今回は基本的なところをやりましたが、オーバーロード関数、テンプレート関数、rvalueの構造体のメンバ、rvalueの配列の要素、コンテナに突っ込んだ場合なども、おいおい調べていきたいと思います。