0/3/5のルール
今回はC++の「0/3/5のルール」について書こうと思います。このルールはクラスを書く時に、以下の要素のどれかを定義するか?を、セットで考えることをお勧するものです。
- デストラクタ
- コピーコンストラクタ
- コピー代入演算子
- ムーブコンストラクタ
- ムーブ代入演算子
なお、普通のコンストラクタは殆どの場合に必要なので、このルールには含まれないです。
0のルール
殆どのクラスはコンストラクタだけを定義すれば十分です。その場合は、上記の要素は定義しない限りデフォルト定義になります。たまに空のデストラクタを見かけますが、カスタムな動作がいらないなら定義しない方がいいです。
デストラクタを定義してしまうと、デフォルトのムーブコンストラクタとムーブ代入演算子が定義されなくなります。「デストラクタが空」という事は、動作はデフォルトでよいという事なので、定義しないでいいと思います。
class rule_of_zero
{
public:
rule_of_zero();
private:
int m_data[10];
};
3のルール(デストラクタ、コピーコンストラクタ、コピー代入演算子)
カスタムなデストラクタが必要な場合は、大体コピーコンストラクタとコピー代入演算子も必要になります。なぜなら、カスタムなデストラクタを定義しているってことは、例えばnew
で作ったヒープの生ポインタをdelete
したり、ファイル記述子をclose()
したりするようなカスタムな解放動作が必要になっている可能性があります。その場合だとデフォルトのコピーコンストラクタとコピー代入演算子がやるシャローコピーは正しくないはずだからです。
生ポインタの場合はポインタがコピーされるだけで、実際のデータはコピーされません。ファイル記述子の場合も、普通のシャローコピーではファイル記述子の変数がコピーされるだけで、実際のファイル記述子がコピーされないため、どちらかのクラスがファイルをclose()
で閉じると、もう一つのクラスでも同じファイル記述子を使っているためファイルが閉じられてしまいます。ですので、カスタムなコピーコンストラクタとコピー代入演算子を定義した方がいいはずです。
class rule_of_three
{
public:
rule_of_three() { m_data = new int[10]; }
~rule_of_three() { delete[] m_data; } // デストラクタ
rule_of_three (const rule_of_three& other) // コピーコンストラクタ
{
copy (other.m_data);
}
rule_of_three& operator= (const rule_of_three& other) // コピー代入
{
if (this != &other) {
delete[] m_data; // 解放
copy (other.m_data);
}
return *this;
}
private:
void copy (const int* data)
{
m_data = new int[10];
std::memcpy (m_data, data, 10);
}
int* m_data;
};
5のルール(デストラクタ、コピーコンストラクタ、コピー代入演算子、ムーブコンストラクタ、ムーブ代入演算子)
C++11 からムーブ機能が追加されました。カスタムなデストラクタ、コピーコンストラクタ、コピー代入演算子のどれかを定義すると、デフォルトのムーブコンストラクタとムーブ代入演算子が消えてしまうので、自分で定義した方がいいです。消えてしまうとstd::move()
した場合にもコピーする動作になってしまいます。
でもこれはムーブの方がコピーよりパフォーマンス的に良いからお勧めされているだけで、3のルール
と違って別に動作的に定義しなくても問題はないです。
class rule_of_five
{
public:
rule_of_five() { m_data = new int[10]; }
~rule_of_five() { delete[] m_data; } // デストラクタ
rule_of_five (const rule_of_five& other) // コピーコンストラクタ
{
copy (other.m_data);
}
rule_of_five& operator= (const rule_of_five& other) // コピー代入
{
if (this != &other) {
delete[] m_data; // 解放
copy (other.m_data);
}
return *this;
}
rule_of_five (rule_of_five&& other) // ムーブコンストラクタ
: m_data (std::exchange (other.m_data, nullptr))
{}
rule_of_five& operator= (rule_of_five&& other) // ムーブ代入
{
std::swap (m_data, other.m_data);
return *this;
}
private:
void copy (const int* data)
{
m_data = new int[10];
std::memcpy (m_data, data, 10);
}
int* m_data;
};
以下のようにムーブコンストラクタ、ムーブ代入演算子を定義しないと、ムーブされずにコピーされてしまいます。
class MyClass
{
public:
MyClass() { m_data = new int[10]; }
~MyClass() { delete[] m_data; } // デストラクタ
MyClass (const MyClass& other) // コピーコンストラクタ
{
copy (other.m_data);
}
MyClass& operator= (const MyClass& other) // コピー代入
{
if (this != &other) {
delete[] m_data; // 解放
copy (other.m_data);
}
return *this;
}
private:
void copy (const int* data)
{
m_data = new int[10];
std::memcpy (m_data, data, 10);
}
int* m_data;
};
MyClass foo{};
MyClass bar{ std::move(foo) }; // コピーコンストラクタ
デフォルト動作の明示、ないしはデフォルト動作の削除
=default
と=delete
を使うと、コンパイラの動作を調整することができます。
// デフォルト動作を明示
class Foo
{
public:
Foo(const Foo&) = default; // コピーコンストラクタ
Foo& operator=(Foo&) = default; // コピー代入演算子
Foo(Foo&&) = default; // ムーブコンストラクタ
Foo& operator=(Foo&&) = default; // ムーブ代入演算子
~Foo() = default; // デストラクタ
};
// デフォルト定義の削除
class Bar
{
public:
Bar(const Bar&) = delete; // コピーコンストラクタ
Bar& operator=(Bar&) = delete; // コピー代入演算子
Bar(Bar&&) = delete; // ムーブコンストラクタ
Bar& operator=(Bar&&) = delete; // ムーブ代入演算子
~Bar() = delete; // デストラクタ
};
=delete
はこの記事とはちょっと違いますが「そのような動作を禁止したい」様な場合に使えると思います。(コピーコンストラクタを禁止したい、など)
=delete
はコンパイラーに判断を任せるという意味で、何も定義しないのと同じですが、その旨を明示的にするために使われます。なお、上記のような読みやすい一覧も書けるため、使うことをお勧めします。
まとめ
要するに、できればコンストラクタだけのクラスが一番いいのですが、カスタムなデストラクタを定義が必要な場合は、コピーコンストラクタとコピー代入演算子も定義しないとバグが発生する可能性が大です。
パフォーマンスが大事な場合は、ムーブコンストラクタとムーブ代入演算子も定義した方がいいです。
Discussion