導入
C++を書く際、デストラクタやコピーコンストラクタを「とりあえず書いておく」という習慣はありませんか?実は、それらの特殊メンバ関数を明示的に定義することは、保守性の低下だけでなく、プログラムの実行速度にも悪影響を及ぼす可能性があります。今回は「Rule of Zero」を遵守することが、なぜコンパイラの最適化やABI(Application Binary Interface)において重要なのかを解説します。
基礎知識
C++には、クラスが特定の条件を満たす場合に「Trivial(自明)」と判定される仕組みがあります。Trivialな型とは、コンパイラから見て「メモリのコピーだけで管理できる単純なデータ」と見なされる型のことです。
重要なのはABI(関数呼び出し規約)です。多くのアーキテクチャでは、Trivialな型を関数に渡す際、メモリを介さずにCPUのレジスタに直接値を乗せて渡すことができます。しかし、ユーザーがデストラクタなどを定義してしまうと、その型は「Non-Trivial」となり、レジスタ渡しができなくなります。結果として、スタック領域にメモリを確保し、そのポインタを経由してデータを渡すという、余分なオーバーヘッドが発生してしまいます。
実装/解決策
解決策はシンプルです。「Rule of Zero」を守ることです。つまり、リソース管理(動的メモリ確保など)が不要なクラスであれば、デストラクタ、コピー/ムーブコンストラクタ、代入演算子を一切定義しない(あるいはすべて `= default` にする)という方針です。これにより、コンパイラは該当する型をTrivialであると判断し、最大限のレジスタ最適化を施すことが可能になります。
サンプルプログラム
以下のコードで、コンパイル後のアセンブリコードがどのように変化するかを想像してみてください。
include
// Rule of Zeroを満たす型
// コンパイラはこれをTrivialと判定し、レジスタ渡しが可能です
struct Optimized {
int id;
double value;
};
// 不必要なデストラクタを定義した型
// これだけでTrivial性が失われ、ABIが「ポインタ渡し」に格下げされます
struct Slow {
int id;
double value;
~Slow() { / 何もしないコードでも、コンパイラには「ユーザー定義」として見える / }
};
void process_optimized(Optimized obj) {
// レジスタに値が直接載るため非常に高速
}
void process_slow(Slow obj) {
// スタックにコピーが発生し、ポインタ経由のアクセスになるため低速
}
int main() {
Optimized o = {1, 3.14};
Slow s = {1, 3.14};
process_optimized(o);
process_slow(s);
return 0;
}
応用・注意点
現場での注意点として、「空のデストラクタを定義する」ことの弊害を理解しておくことが重要です。特に、ヘッダオンリーライブラリなどで頻繁に呼び出されるデータ構造に不必要なデストラクタを記述すると、呼び出し元すべてでABIが劣化し、アプリケーション全体でパフォーマンスの低下を招く恐れがあります。
また、もし「デストラクタは必要だが、Trivialな性質は維持したい」という特殊なケースがあれば、C++11以降の `= default` を利用しましょう。これはコンパイラに「デフォルトの振る舞い」を明示するものであり、ユーザー定義のコードが存在しないものとして扱われるため、Trivial性を維持できる場合があります。
「必要ないなら書かない」。このモダンC++の鉄則は、コードの行数を減らすだけでなく、CPUをより効率的に働かせるための強力なチューニング手法なのです。

コメント