空類就是沒有靜態(tài)成員變量的類,卻通常帶有 typedef 和成員函數(shù)。
空類運行時占用的空間
為保證不同的對象的地址是不同的,C++ 要求空類的大小不能為零。
class Empty { };
int main()
{
std::cout << "sizeof(Empty): " << sizeof(Empty) << '\n';
Empty arr[10];
std::cout << "sizeof(arr): " << sizeof(arr) << '\n';
Empty a, b;
if (&a != &b) {
std::cout << "the size of class Empty is not zero" << '\n';
}
}
上述代碼結(jié)果如下(本文的測試環(huán)境為 Ubuntu-16.04-64bit GCC-5.4.0):
sizeof(Empty): 1
sizeof(arr): 10
the size of class Empty is not zero
如果 Empty 的大小為0,則無法區(qū)別 arr 中的十個元素。對于多數(shù)平臺,Empty 的大小都是1,但是部分平臺在對齊上有著較為嚴格的要求,結(jié)果可能會是一個字的大小(比如8)。
對于帶有虛函數(shù)的空類:
class EmptyWithVirtualFunc
{
public:
virtual void VirtualFunc() { }
};
int main()
{
std::cout << "sizeof(EmptyWithVirtualFunc): " << sizeof(EmptyWithVirtualFunc) << '\n';
std::cout << "sizeof(void*): " << sizeof(void*) << '\n';
}
結(jié)果如下:
sizeof(EmptyWithVirtualFunc): 8
sizeof(void*): 8
帶有虛函數(shù)的空類,編譯器會在該空類對象的起始位置(所有非靜態(tài)成員變量之前)放置一個虛指針,所以該類的大小不是1而是一個指針的大小。
空基類優(yōu)化
在 C++ 中有一個現(xiàn)象與上述相悖:在空類作為基類的情況下,子類的空間中可能不會出現(xiàn)多出來的那一個字節(jié)。 由于帶有虛函數(shù)的空類實質(zhì)上還是有一個隱藏的虛指針成員,不算是嚴格意義上的空類,所以不參與空基類優(yōu)化。
單繼承
class Derived1 : public Empty { };
class Derived2 : public Empty
{
public:
std::int32_t i32;
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Devired1) << '\n';
std::cout << "sizeof(Derived2): " << sizeof(Devired2) << '\n';
}
結(jié)果如下:
sizeof(Derived1): 1
sizeof(Derived2): 4
從結(jié)果可以看出,Empty 在沒有繼承情況下多出來的一個字節(jié)在子類中并沒有體現(xiàn),這一個字節(jié)被“優(yōu)化”了。
當有子類繼承空類 Derived1,既多層繼承時:
class Derived3 : public Derived1 { };
class Derived4 : public Derived1
{
public:
std::int32_t i32;
};
int main()
{
std::cout << "sizeof(Derived3): " << sizeof(Devired3) << '\n';
std::cout << "sizeof(Derived4): " << sizeof(Devired4) << '\n';
}
結(jié)果如下:
sizeof(Derived3): 1
sizeof(Derived4): 4
從多層繼承的結(jié)果可以看出,多出的那一個字節(jié)是否被優(yōu)化與空類的繼承層數(shù)無關(guān)。
但是在部分情況下,優(yōu)化效果會消失:
class Derived5 : public Empty
{
public:
Empty e;
};
class Derived6 : public Empty
{
public:
static Empty se;
};
Empty Derived6::se { };
class Derived7 : public Empty
{
public:
std::int32_t i32;
Empty e;
};
int main()
{
std::cout << "sizeof(Derived5): " << sizeof(Devired5) << '\n';
std::cout << "sizeof(Derived6): " << sizeof(Devired6) << '\n';
std::cout << "sizeof(Derived7): " << sizeof(Devired7) << '\n';
}
結(jié)果如下:
sizeof(Derived5): 2
sizeof(Derived6): 1
sizeof(Derived7): 8
我們分析一下這三個子類的內(nèi)存布局,
Derived5:
此時空基類優(yōu)化失去了效果。如果依然進行優(yōu)化,則無法區(qū)分基類 Empty 和子類中的成員 Empty(注意子類 Derived5 中的 Empty 不是基類,所以不參與優(yōu)化,一定會占用一個字節(jié))。
Derived6:
依然進行空基類優(yōu)化。因為靜態(tài)成員變量不屬于某個具體的類實例,不占用類實例的空間,所以此時基類 Empty 不會與靜態(tài)成員變量發(fā)生沖突,但是由于 Derived6 是空類,所以還是要占用一個字節(jié)空間。
Derived7:
依然進行了空基類優(yōu)化。因為基類 Empty 與子類中的成員 Empty 的地址空間不是相連的,不發(fā)生沖突(注意此時優(yōu)化掉了基類 Empty 的一個字節(jié),并沒有優(yōu)化子類成員變量 Empty)。在子類成員 Empty 后補齊三個字節(jié),所以整體占用的空間是八個字節(jié)。
多重繼承
如果不同的空類同時作為一個類的基類時,
class Empty1 { };
class MultiDerived : public Empty, public Empty1 { };
int main()
{
std::cout << "sizeof(MultiDerived): " << sizeof(MultiDerived) << '\n';
}
結(jié)果如下:
sizeof(MultiDerived): 1
編譯器認為不同的空類在子類的內(nèi)存空間是不會發(fā)生沖突的。
再考慮如下的情況,
class MultiDerived1 : public Empty { };
class MultiDerived2 : public Empty { };
class MultiDerived3 : public MultiDerived1, public MultiDerived2 { };
int main()
{
std::cout << "sizeof(MultiDerived3): " << sizeof(MultiDerived3) << '\n';
}
結(jié)果如下(暫不考慮虛繼承):
sizeof(MultiDerived3): 2
MultiDerived3的內(nèi)存布局如下:
沒有進行空基類優(yōu)化。由于 MultiDerived1 是一個(is-a)Empty,而且 MultiDerived2 也是一個 Empty,又由于 MultiDerived1 和 MultiDerived2 在子類的內(nèi)存空間中是連續(xù)的,此時如果進行了空基類優(yōu)化,則兩個 Empty 就無法區(qū)分。
再考慮如下的情況,
class NotEmpty
{
public:
std::int32_t i32;
};
class MultiDerived4 : public MultiDerived1, public NotEmpty
{
public:
Empty e;
};
class MultiDerived5 : public NotEmpty, public MultiDerived1
{
public:
Empty e;
};
class MultiDerived6 : public NotEmpty, public MultiDerived1 { };
int main()
{
std::cout << "sizeof(MultiDerived4): " << sizeof(MultiDerived4) << '\n';
std::cout << "sizeof(MultiDerived5): " << sizeof(MultiDerived5) << '\n';
std::cout << "sizeof(MultiDerived6): " << sizeof(MultiDerived6) << '\n';
}
結(jié)果如下:
sizeof(MultiDerived4): 8
sizeof(MultiDerived5): 8
sizeof(MultiDerived6): 4
我們分析一下這三個子類的內(nèi)存布局,
MultiDerived4:
進行了空基類優(yōu)化。由于 MultiDerived1 的 Empty 與子類成員 Empty 中間隔了 NotEmpty,所以不發(fā)生沖突,因此可以進行優(yōu)化。
MultiDerived5:
沒有發(fā)生空基類優(yōu)化。因為 MultiDerived1 的 Empty 與子類成員 Empty 是連續(xù)的,進行優(yōu)化會發(fā)生沖突。
MultiDerived6:
進行了空基類優(yōu)化。因為 MultiDerived1 的 Empty 不會與其他 Empty 發(fā)生沖突。
特殊的情況
再來看看比較特殊的情況,
class Foo
{
public:
Empty e[4];
Derived2 d;
};
class Foo1Helper : public Empty
{
public:
std::int8_t i8[3];
};
class Foo1 : public Empty
{
public:
Foo1Helper d;
};
class Foo2 : public Empty
{
public:
Foo f;
};
int main()
{
std::cout << "sizeof(Foo): " << sizeof(Foo) << '\n';
std::cout << "sizeof(Foo1): " << sizeof(Foo1) << '\n';
std::cout << "sizeof(Foo2): " << sizeof(Foo2) << '\n';
}
結(jié)果如下:
sizeof(Foo): 8
sizeof(Foo1): 4
sizeof(Foo2): 12
Foo 中的 Derived2 仍然進行了空基類優(yōu)化,并沒有因為 Foo 中的成員 Empty 與 Derived2 的基類 Empty 相鄰而影響優(yōu)化,從“空基類優(yōu)化”這個名字也表明了該優(yōu)化只與繼承體系有關(guān)系,而不考慮被優(yōu)化的類之外的干擾。
Foo1 也進行了空基類優(yōu)化,但是比較特別,編譯器首先考慮的是將子類成員變量 Foo1Helper 進行優(yōu)化(理由同 Foo1 中的 Derived2),此時 Foo1Helper 內(nèi)存空間中已不存在 Empty,所以也對 Foo1 進行了優(yōu)化。
Foo2 沒有發(fā)生空基類優(yōu)化,因為第一個成員 Foo 的第一個成員變量是 Empty,與基類中 Empty 發(fā)生了沖突。
結(jié)論
當空類作為一個類的基類的時候,該空類占用的額外一個字節(jié)的內(nèi)存空間在子類中將會被優(yōu)化掉,除了一種情況外:在子類的內(nèi)存空間中有連續(xù)的相同類型的空類出現(xiàn)時(無論該空類是作為基類,超基類,子類的第一個非靜態(tài)成員變量,子類的第一個非靜態(tài)成員變量的基類,子類的第一個非靜態(tài)成員變量的成員,所有的這些都可以歸納為子類的內(nèi)存空間中基類的空間與接下來的第一個內(nèi)存塊),為了區(qū)分連續(xù)的空類,將不進行空基類優(yōu)化。
此外,在 C++11 中,空基類優(yōu)化是強制性的,不再是可選的。
空類的應(yīng)用
std::vector
在標準庫中,使用到分配器(allocator-aware)的類大多利用到了空基類優(yōu)化,進而避免無狀態(tài)(stateless)的分配器成員占用額外的空間。
template <typename _Tp, typename _Alloc>
struct _Vector_base
{
typedef _Alloc<_Tp> _Tp_alloc_type; // 分配器的具體類型
typedef _Tp* pointer; // 存儲類型
// 數(shù)據(jù)存儲的具體實現(xiàn)
struct _Vector_impl : public _Tp_alloc_type
{
pointer _M_start; // 存儲的開始
pointer _M_finish; // 存儲的結(jié)束
pointer _M_end_of_storage; // 已經(jīng)分配的空間,即capacity
};
};
template <typename _Tp, typename _Alloc = std::allocator<_Tp>>
class vector : protected _Vector_base<_Tp, _Alloc>
{
};
以上是經(jīng)過簡化的 std::vector 的代碼。我們需要關(guān)注的是 _Vector_base 中的 _Vector_impl。
對于 _Tp_alloc_type,我們也可以不讓 _Vector_impl 繼承于 _Tp_alloc_type,單獨設(shè)置一個成員變量,
template <typename _Tp, typename _Alloc>
struct _Vector_base
{
typedef _Alloc<_Tp> _Tp_alloc_type; // 分配器的具體類型
typedef _Tp* pointer; // 存儲類型
// _Vector_impl利用該變量進行內(nèi)存的分配
_Tp_alloc_type _alloc;
// 數(shù)據(jù)存儲的具體實現(xiàn)
struct _Vector_impl
{
pointer _M_start; // 存儲的開始
pointer _M_finish; // 存儲的結(jié)束
pointer _M_end_of_storage; // 已經(jīng)分配的空間,即capacity
};
};
由于無狀態(tài)的分配器是空類,沒有任何成員變量,這樣處理的話會白白浪費了一個字節(jié)的存儲空間,像std::vector 這樣的使用率非常高的類來說,代價非常高。
所以標準庫采用了空基類優(yōu)化,將分配器額外的存儲空間優(yōu)化掉。
std::enable_if
template <bool _Cond, typename _Tp = void>
struct enable_if { };
template <typename _Tp>
struct enable_if<true, _Tp>
{
typedef _Tp type;
};
以上是 GCC 中關(guān)于 std::enable_if 完整的代碼。
enable_if 是空類,但是這里與空基類優(yōu)化無關(guān)。當 _Cond 為 true 時,enable_if 進行了部分模板特化,其中的 typedef 是關(guān)鍵。
下面是 enable_if 的實例,
template <typename T>
typename std::enable_if<std::is_integral<T>::value,bool>::type is_odd(T i)
{
return (i%2) == 1;
}
template <typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even(T i)
{
return (i%2) == 0;
}
int main()
{
int i { 2 }; // i是整型值
std::cout << std::boolalpha; // bool值會展示成"true", "false"而不是"0", "1"
std::cout << "i is odd: " << is_odd(i) << '\n';
std::cout << "i is even: " << is_even(i) << '\n';
double d { 2.0 }; // d是雙精度浮點數(shù)
std::cout << "i is odd: " << is_odd(i) << '\n'; // ERROR, 編譯失敗
std::cout << "i is even: " << is_even(i) << '\n'; // ERROR, 編譯失敗
}
結(jié)果如下:
i is odd: false
i is even: true
在上述的兩個例子中,_Cond 為 true 的模板特化中的 type 成為了關(guān)鍵,如果 _Cond 為 false,則使用type 會發(fā)生編譯錯誤,因為在原型中沒有 type。is_odd 利用的 type 作為返回值;is_even 則純粹是利用 type 作為編譯時的驗證工具。
利用空類替代friend
關(guān)鍵字 friend 是一種強耦合,甚至強于繼承,所以我們應(yīng)當小心地使用 friend 或者盡量避免。
friend 的常見用途是訪問另一個類的私有構(gòu)造函數(shù),
class Secret
{
friend class SecretFactory;
private:
// SecretFactory可以訪問該構(gòu)造函數(shù)
explicit Secret(std::string str) : _data{std::move(str)} {}
// SecretFactory同時也可以訪問該函數(shù),但是這可能會給我們造成麻煩
void addData(const std::string& moreData) { _data.append(moreData); }
private:
// SecretFactory無論如何也不應(yīng)該訪問該數(shù)據(jù)
std::string _data;
};
在上述例子中,SecretFactory 可以訪問不該訪問的 _data,這會添加很多麻煩。
我們可以通過空類來限制 SecretFactory 可以訪問的函數(shù),
class Secret
{
public:
class ConstructorKey {
// 如果其他的類想要訪問Secret的構(gòu)造函數(shù),可以在這里添加友元
friend class SecretFactory;
private:
// 構(gòu)造函數(shù)為private很關(guān)鍵
ConstructorKey() {}; // ①
ConstructorKey(const ConstructorKey&) = default; // ②
};
// 設(shè)置為public是為了讓SecretFactory訪問
explicit Secret(std::string str, ConstructorKey) : _data{std::move(str)} {}
private:
void addData(const std::string& moreData) { _data.append(moreData); }
std::string _data;
};
class SecretFactory
{
public:
Secret getSecret(std::string str) {
// RVO
return Secret { std::move(str), Secret::ConstructorKey{} };
}
void modify(Secret& secret, const std::string& additionalData) {
// secret.addData(additionalData); // ERROR, addData是私有的,此時空類已經(jīng)限制了SecretFactory訪問Secret的函數(shù)
}
};
int main()
{
// Secret s { "Secret Class", ConstructorKey{} }; // ERROR, 無法訪問ConstructorKey的構(gòu)造函數(shù)
SecretFactory sf;
Secret s = sf.getSecret("Secret Class");
}
上例有兩點需要解釋,
對于①,ConstructorKey 的構(gòu)造函數(shù)的訪問權(quán)限是 private,只有對其為 friend 的類才能訪問構(gòu)造函數(shù);不能將構(gòu)造函數(shù)設(shè)置為 default,即 ConstructorKey() = default;,對于沒有非靜態(tài)成員的類(空類)來講,即使默認構(gòu)造為 private,依然可以通過統(tǒng)一初始化方式(uniform initialization)對其進行初始化,
class EmptyUniIni
{
EmptyUniIni() = default;
};
int main()
{
EmptyUniIni empty; // ERROE, 無法訪問構(gòu)造函數(shù)
EmptyUniIni empty1 {}; // OK, uniform initialization
}
對于②,需要將復(fù)制構(gòu)造函數(shù)設(shè)置為 private,否則的話可以通過下面的代碼進行構(gòu)造 Secret,
Secret::ConstructorKey* pk = nullptr;
Secret s { "Secret class", *pk };
這樣的話,我們前邊所做的努力就白費了。
std::input_iterator_tag, std::output_iterator_tag
// 用來標記input iterator
struct input_iterator_tag { };
// 用來標記output iterator
struct output_iterator_tag { };
// _Category即上述兩個標簽
template <typename _Category, typename... _Others>
struct iterator { };
// 簡略寫其他的template參數(shù)
template <typename... _Others>
class istream_iterator : public iterator<input_iterator_tag, _Others...>
{
};
template <typename... _Others>
class ostream_iterator : public iterator<output_iterator_tag, _Others...>
{
};
上述是簡化的 GCC 代碼。
由于 C++ 是強類型語言,input_iterator_tag 和 output_iterator_tag 雖然什么都沒有,只有名字不同,他們也是不同的類型,所以 istream_iterator 的父類和 ostream_iterator 的父類是不同的,他們在繼承層次上沒有任何關(guān)系,即 input_iterator_tag 標記了 istream_iterator,output_iterator_tag 標記了ostream_iterator。
而且上述代碼不會有任何性能上的缺陷,因為編譯器會檢查模板中的參數(shù)是否被使用,如果沒有使用,則將該模板參數(shù)省略掉,進而不會影響性能。
總結(jié)
通過上述講解,我們了解了空類的一些特性與應(yīng)用場景,利用空基類優(yōu)化或者與模板結(jié)合起來,會有奇妙的效果。
參考
[1] C++ Templates: The Complete Guide
[2] classes-and-objects
[3] Passkey Idiom: More Useful Empty Classes
[4] The "Empty Member" C++ Optimization
[5] Empty base optimization