【C++学習|豆知識】C++20の隠れたメモリ節約術:[[no_unique_address]]で空クラスを最適化する

導入

C++でクラス設計を行う際、アロケータやポリシーなどの「状態を持たないクラス(空クラス)」をメンバとして保持することはよくあります。しかし、C++の言語仕様上、空クラスであっても通常は1バイトのサイズを占有します。これにより、小さなデータ構造が不必要に肥大化してしまうという課題がありました。C++20で導入された [[no_unique_address]] は、この無駄なパディングを排除し、メモリ効率を劇的に改善する非常に強力な属性です。

基礎知識

C++には「オブジェクトは一意のアドレスを持つ」という原則があります。そのため、空クラスであってもインスタンス化すれば識別できるように、コンパイラは少なくとも1バイトの領域を割り当てます。
従来、これを回避する唯一の方法は EBCO (Empty Base Class Optimization:空基底クラス最適化) でした。しかし、これはクラスを継承関係にする必要があり、設計が複雑になるという欠点がありました。[[no_unique_address]] は、この最適化を「メンバ変数(コンポジション)」の形でも実現可能にする画期的な機能です。

実装/解決策

使い方は非常にシンプルで、最適化したいメンバ変数の前に属性を記述するだけです。これにより、コンパイラに対して「このメンバは他のメンバとアドレスを共有しても良い(一意でなくても良い)」という指示を与えることができます。ただし、「同じ型を複数並べると、それぞれのインスタンスは異なるアドレスを持つ必要がある」というC++の制約は残るため、注意が必要です。

サンプルプログラム

以下のコードをコンパイルして実行すると、[[no_unique_address]] の有無でクラスのサイズがどう変わるかを確認できます。

include

// 状態を持たない空クラス
struct Empty {};

// 属性なし:sizeof(Empty) が 1 バイト加算される
struct StandardClass {
Empty e;
int x;
};

// 属性あり:Empty が最適化され、int と同じサイズになる
struct OptimizedClass {
[[no_unique_address]] Empty e;
int x;
};

int main() {
std::cout << "StandardClass のサイズ: " << sizeof(StandardClass) << " バイト" << std::endl; std::cout << "OptimizedClass のサイズ: " << sizeof(OptimizedClass) << " バイト" << std::endl; return 0; }

応用・注意点

現場で活用する際の重要な注意点は以下の2点です。

1. 同じ型のメンバを複数置かない
同じ型を2つ並べると、当然ながら一意のアドレスが必要になるため、両方を最適化することはできません。
2. コンパイラのサポート状況
C++20規格ですが、一部の古いコンパイラや設定では無視される場合があります。また、デバッグ時にアドレスが完全に重なることで、ポインタの比較やデバッグ表示に混乱が生じることがあります。

この属性は、テンプレートメタプログラミングを多用するライブラリ作成者や、メモリ制限の厳しい組み込み開発において特に威力を発揮します。設計を継承に頼らず、クリーンなコンポジションを維持したままパフォーマンスを向上させられるため、積極的に活用していきましょう。

コメント

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