【C++学習|初心者向け】C++のメモリ効率を極める!空の削除子最適化(EBO)を理解しよう

1. 導入:なぜスマートポインタのサイズが重要なのか

C++でメモリ管理を行う際、std::unique_ptrは非常に強力なツールです。しかし、独自の削除子(デリータ)を指定した際、「なぜかクラスのサイズが想定より大きくなってしまった」という経験はありませんか?
実は、削除子が「状態を持たない(空の)クラスやラムダ式」である場合、C++にはそのサイズを削減する「空の基底クラス最適化(EBO: Empty Base Optimization)」という仕組みが働きます。これを理解することで、メモリ消費を最小限に抑えた堅牢なプログラムを書くことができます。

2. 基礎知識:EBOとは何か

C++では、通常、空のクラスであっても、オブジェクトとして存在するために最低でも1バイトのサイズが割り当てられます。しかし、あるクラスが別のクラスを継承している場合、親クラスが空であれば、そのサイズを0バイトとして扱う最適化がなされます。
std::unique_ptrは内部で削除子を保持していますが、この削除子が「空」であれば、EBOが適用され、ポインタ本体のサイズ(通常8バイト)を増やさずに済むのです。

3. 実装と解決策

std::unique_ptrを使用する際、削除子をテンプレート引数として渡すことができます。このとき、削除子を「データを持たない構造体」や「キャプチャなしのラムダ式」として定義するのがポイントです。もし削除子にメンバ変数を持たせてしまうと、EBOが効かなくなり、unique_ptrのサイズが膨らんでしまいます。

4. サンプルプログラム

以下のコードを実行して、削除子の有無によるサイズの違いを確認してみましょう。

include
include

// 空の削除子:状態を持たないためEBOが適用される
struct MyDeleter {
void operator()(int p) const {
delete p;
}
};

// サイズを持つ削除子:EBOが適用されず、unique_ptrが肥大化する
struct BigDeleter {
int dummy; // 状態を持つためサイズが増える
void operator()(int p) const {
delete p;
}
};

int main() {
// 空の削除子の場合:ポインタのサイズ(8バイト)を維持
std::unique_ptr ptr1(new int(10));

// サイズを持つ削除子の場合:ポインタのサイズ + 削除子のサイズになる
std::unique_ptr ptr2(new int(20), BigDeleter{1});

std::cout << "空の削除子のサイズ: " << sizeof(ptr1) << " バイト" << std::endl; std::cout << "重い削除子のサイズ: " << sizeof(ptr2) << " バイト" << std::endl; return 0; }

5. 応用・注意点

現場での開発において、特に気をつけるべきは「キャプチャ付きのラムダ式」です。ラムダ式に変数をキャプチャ([x]など)させると、そのラムダは「状態を持つ」とみなされ、EBOが機能しなくなります。
メモリ効率を最優先したい場合、削除子はできる限りステートレス(状態を持たない)にする設計を心がけましょう。もしどうしても状態が必要な場合は、削除子自体をスマートポインタの外部で管理するか、std::shared_ptrの利用を検討するなど、トレードオフを意識することが重要です。

コメント

タイトルとURLをコピーしました