「空のクラス」を定義します。
Sky classという意味ではありませんよ。Empty classという意味です。
メンバ変数を持たず継承もしていない、とにかくデータとしての要素の無いクラスです。
1 |
class EmptyClass{}; |
このEmptyClassのサイズはいくつでしょうか?
Emptyという位なのでゼロなのかと思いきや、sizeofで計るとゼロ以外の数値になります。
sizeof(Empty) = 1ですね。
なぜか?その答えはある名曲に隠されています。
言葉はいつも奥の方から後ろに虚しさ連れて教えてくれた
けれどこんなにもからっぽになったのに僕は歩き出した__ゆず「からっぽ」
からっぽに見えても、みんな何かを抱えて生きているのです。ゼロなんてありえないんですね。C++のクラスも同じなんですね。
納得感を得たところで、改めて言うと、C++の型は全くデータメンバが無い状態でも単独でインスタンスが作られる場合必ずサイズを持ちます。
C++のテクニカルな理由で、「空のクラス」でも、「独立したオブジェクト」のサイズは0にできないのです。
__Effective C++ 第3版
「テクニカルな理由」というのは、インスタンス化された際にオブジェクトのアドレスをとれるようにする為です。サイズ0ではアドレスが決まりませんからね。
このようにc++では単独でインスタンス化されるオブジェクトは例外なくサイズを持ちます。
例えばメンバ変数の一部として定義された場合もサイズを持ちます。
1 2 3 4 |
class Hoge { int i; EmptyClass e; }; |
この場合でも&Hoge::eを決めなければいけないのでeはサイズを持ちます。結果として sizeof(Hoge) > sizeof(int) となります。
たった1byteとはいえ無駄なメモリを消費することになります。
コピー禁止をmix-inするboost::noncpyable等はまさにメンバを持たない空のクラスですね。コピー禁止するたびにデータサイズが増加したらたまったものではありません。
そんなわけで、規格では、空の型が別の型を継承していて、継承元の型が既にサイズを持っている場合や、逆に空の型の継承先がサイズを持っている場合はEmptyClass自体の追加のサイズは無くしても良いことになっています。
1 2 3 4 5 6 7 8 9 |
#include <iostream> class Int { int i; }; class EmptyClass : Int{}; int main() { std::cout << sizeof(EmptyClass) << ":" << sizeof(int) << std::endl; return 0; } |
1 |
[出力]4:4 |
型を継承した時に、追加のサイズを持たせない最適化のことをEBO(Empty Base Optimization)と言います。
EBOの効かないコンパイラなんてほとんどありませんが、場合によっては型サイズの肥大化やアライメントのズレの原因になるので注意する必要があります。
EBOを意識した実装
コンパイラが空のクラスを最適化してくれることは理解しました。
プログラマもEBOが効くコードを意識して書きましょう。
先のboost::noncopyableも空のクラスでしたが、他にも空のクラスを定義するシチュエーションは数多くあります。
例えばファンクタなんかも、往々にしてoperator()だけ定義して、メンバ変数も特に無いというケースがあるかと思います。
スマートポインタのデリータやコンテナのアロケータを渡して実装を決めるテンプレートクラスもEBOを意識した設計にすることでメモリ効率が上がります。
以下が一例になります。オリジナルのコンテナクラスにテンプレートパラメータとしてアロケータを渡す実装で考えてみましょう。
1 2 3 4 5 6 7 8 9 10 |
// ユーザーが定義したアロケータ class MyAllocator { public: void* allocate(std::size_t size) { return std::malloc(size); } // アロケータの正に画期的な実装を思いついたが // それを示すにはこの余白は狭すぎる。 // よってallocateメソッド以外のあらゆる定義を省くことにした。 }; |
まずはEBOを意識しない設計。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// EBOのことを考えてないクラス template <class T, class Allocator> class ContainerNotEBO { // メンバ T* data_ = nullptr; std::size_t capacity_; Allocator allocator_; // アロケータをメンバとして持つ public: // 領域確保 ContainerNotEBO(std::size_t capacity) : capacity_(capacity) , allocator_() , data_(nullptr) { data_ = reinterpret_cast<T*>(allocator_.allocate(capacity * sizeof(T))); // メンバのallocateを呼び出す } // 以下コンテナとしての必要条件を満たす色々な機能の定義が続く... }; |
普通にAllocatorをメンバとして持っています。
続いてEBOを意識した設計。
Allocatorを継承した設計になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// EBOのことを考えているクラス template <class T, class Allocator> class ContainerEBO : public Allocator // 継承することでEBOを効かせる { // メンバ T* data_ = nullptr; std::size_t capacity_; public: // 領域確保 ContainerEBO(std::size_t capacity) : capacity_(capacity) , data_(nullptr) { data_ = reinterpret_cast<T*>(this->allocate(capacity * sizeof(T))); // 継承したのでallocateメソッドは自身が持つ } // 以下コンテナとしての必要条件を満たす色々な機能の定義が続く... }; |
サイズを計ってみましょう。Wandboxで確認
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { using NotEBO = ContainerNotEBO<int, MyAllocator>; using EBO = ContainerEBO<int, MyAllocator>; std::cout << "Not EBO " << sizeof(NotEBO) << std::endl; std::cout << "EBO " << sizeof(EBO) << std::endl; return 0; } |
1 2 3 |
[出力] Not EBO 24 EBO 16 |
このようにちょっと意識することでメモリ効率の良いクラス設計ができるようになります。
まとめ
- C++では単独でインスタンス化されるオブジェクトは全て1以上のサイズを持つ
- サイズを持つ理由はアドレスを決定できるようにするため
- 他にサイズを持っている型と継承関係を持つことで空の型のサイズは最適化される
- この最適化をEBOという
- ちょっとEBOを意識したクラス設計にすると「解ってる感」が演出されて良い