MSVCでempty base optimizationの効かないパターンと対策

問題

2つ以上の空クラスを継承するようなクラスについて、MSVCで empty base optimization (EBO) が期待した通りに働かず、余分な領域が消費される。

struct Empty1 {};
struct Empty2 {};

// 1つの空クラスを基底に持つ
struct Derived1 : Empty1 {
    int i;
};

// 2つの空クラスを基底に持つ
struct Derived2 : Empty1, Empty2 {
    int i;
};

static_assert(sizeof(Empty1) == 1);
static_assert(sizeof(Empty2) == 1);

static_assert(sizeof(Derived1) == sizeof(int));
static_assert(sizeof(Derived2) == sizeof(int)); // MSVCにてエラー

対策

__declspec(empty_bases)拡張属性をEBOを期待するクラスの宣言に付与する。

struct __declspec(empty_bases) Derived2 : Empty1, Empty2 {
    int i;
};

static_assert(sizeof(Derived2) == sizeof(int));

参考: Optimizing the Layout of Empty Base Classes in VS2015 Update 2 | Visual C++ Team Blog

empty base optimization

C++では空クラス(メンバ変数を持たず、仮想メンバ関数を持たないクラス)のサイズは必ず 1以上の大きさを持ちます。 (現行のほとんどの環境で空クラスのサイズは1になります)

struct Empty {};

static_assert(sizeof(Empty) > 0); // 多くの環境で sizeof(Empty) == 1

EBOはその様な空クラスを継承する際のメモリレイアウトに関する最適化の一種で、 継承先のクラスに余分な領域を確保しないようにする効果があります。

struct Empty {}; // 空クラス
struct NotEmpty { char c; }; // 空でないクラス

// どちらもサイズは1byte
static_assert(sizeof(Empty)    == 1);
static_assert(sizeof(NotEmpty) == 1);

// 基底クラスが空クラスなのでEBOが効く
struct A : Empty { int i; };

static_assert(sizeof(A) == sizeof(int));

// 基底クラスが空でないので
// メンバ変数と基底クラスの領域 (+ アライメントのためのパディング)分のサイズを必要とする
struct B : NotEmpty { int i; };

static_assert(sizeof(B) > sizeof(int));

例えばsizeof(int) == 4, alignof(int) == 4であるようなある環境ではsizeof(A) == 4, sizeof(B) == 8となりました。

MSVCでの動作

MSVCではただ1つの空クラスを継承するようなクラスに関して他のコンパイラ同様EBOが働きますが、 2つ以上の空クラスを継承する場合 (デフォルトでは) EBOが働きません

上に挙げた参考サイトの記述によると、

The Visual C++ compiler has historically had limited support for EBCO; however, in Visual Studio 2015 Update 2, we have added a new __declspec(empty_bases) attribute for class types that takes full advantage of this optimization.

VS2015 Update 2 以降では クラスの宣言に__declspec(empty_bases)を付与することで複数の空クラスを継承する場合にもEBOが有効になる様です。

struct Empty1 {};
struct Empty2 {};

// 定義に __declspec(empty_bases) を付ける
struct __declspec(empty_bases) Derived1 : Empty1, Empty2 { int i; };

static_assert(sizeof(Derived1) == sizeof(int)); // EBOが効くようになった

// または前方宣言に __declspec(empty_bases) を付ける
struct __declspec(empty_bases) Derived2;

struct Derived2 : Empty1, Empty2 { int i; };

static_assert(sizeof(Derived2) == sizeof(int)); // EBOが効くようになった

MSVCに於いてこのような場合のEBOが依然としてデフォルトで有効にならないのはABI互換性を保つためのようです。

他のコンパイラでもビルドを出来るようにするにはマクロを使った工夫が必要になるでしょう。

#if defined(_MSC_VER) && _MSC_FULL_VER >= 190023918
#  define EMPTY_BASES __declspec(empty_bases) // VS2015 Update 2 以降
#else
#  define EMPTY_BASES
#endif

struct EMPTY_BASES Derived : Empty1, Empty2 { int i; };

static_assert(sizeof(Derived) == sizeof(int));

見た目がダサい

どのような場合に問題になるか

クラス内部のメモリレイアウトに依存したプログラムを書くような場合、EBOが効かないことが問題となります。

理想的にはクラスのメモリレイアウトに極力依存しないことが理想ではありますが、実際問題としてC/C++では特定のメモリレイアウトを期待するようなプログラムを書く必要に迫られる場面も少なからず存在します。

そのような時、プログラムが予想と違う動作をした場合には実際にメモリ上にどの様にクラスが展開されているのかを注意深く確認しましょう。