空類就是沒有靜態(tài)成員變量的類店诗,卻通常帶有 typedef 和成員函數(shù)。
空類運(yùn)行時(shí)占用的空間
為保證不同的對(duì)象的地址是不同的,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é)果如下(本文的測(cè)試環(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 中的十個(gè)元素。對(duì)于多數(shù)平臺(tái)躁染,Empty 的大小都是1鸣哀,但是部分平臺(tái)在對(duì)齊上有著較為嚴(yán)格的要求,結(jié)果可能會(huì)是一個(gè)字的大型掏(比如8)我衬。
對(duì)于帶有虛函數(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ù)的空類,編譯器會(huì)在該空類對(duì)象的起始位置(所有非靜態(tài)成員變量之前)放置一個(gè)虛指針饰恕,所以該類的大小不是1而是一個(gè)指針的大小低飒。
空基類優(yōu)化
在 C++ 中有一個(gè)現(xiàn)象與上述相悖:在空類作為基類的情況下,子類的空間中可能不會(huì)出現(xiàn)多出來的那一個(gè)字節(jié)懂盐。 由于帶有虛函數(shù)的空類實(shí)質(zhì)上還是有一個(gè)隱藏的虛指針成員,不算是嚴(yán)格意義上的空類糕档,所以不參與空基類優(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 在沒有繼承情況下多出來的一個(gè)字節(jié)在子類中并沒有體現(xiàn)速那,這一個(gè)字節(jié)被“優(yōu)化”了俐银。
當(dāng)有子類繼承空類 Derived1,既多層繼承時(shí):
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é)果可以看出端仰,多出的那一個(gè)字節(jié)是否被優(yōu)化與空類的繼承層數(shù)無關(guān)捶惜。
但是在部分情況下,優(yōu)化效果會(huì)消失:
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
我們分析一下這三個(gè)子類的內(nèi)存布局荔烧,
Derived5:
此時(shí)空基類優(yōu)化失去了效果吱七。如果依然進(jìn)行優(yōu)化,則無法區(qū)分基類 Empty 和子類中的成員 Empty(注意子類 Derived5 中的 Empty 不是基類鹤竭,所以不參與優(yōu)化踊餐,一定會(huì)占用一個(gè)字節(jié))。
Derived6:
依然進(jìn)行空基類優(yōu)化臀稚。因?yàn)殪o態(tài)成員變量不屬于某個(gè)具體的類實(shí)例吝岭,不占用類實(shí)例的空間,所以此時(shí)基類 Empty 不會(huì)與靜態(tài)成員變量發(fā)生沖突,但是由于 Derived6 是空類窜管,所以還是要占用一個(gè)字節(jié)空間散劫。
Derived7:
依然進(jìn)行了空基類優(yōu)化。因?yàn)榛?Empty 與子類中的成員 Empty 的地址空間不是相連的幕帆,不發(fā)生沖突(注意此時(shí)優(yōu)化掉了基類 Empty 的一個(gè)字節(jié)获搏,并沒有優(yōu)化子類成員變量 Empty)。在子類成員 Empty 后補(bǔ)齊三個(gè)字節(jié)蜓肆,所以整體占用的空間是八個(gè)字節(jié)颜凯。
多重繼承
如果不同的空類同時(shí)作為一個(gè)類的基類時(shí),
class Empty1 { };
class MultiDerived : public Empty, public Empty1 { };
int main()
{
std::cout << "sizeof(MultiDerived): " << sizeof(MultiDerived) << '\n';
}
結(jié)果如下:
sizeof(MultiDerived): 1
編譯器認(rèn)為不同的空類在子類的內(nèi)存空間是不會(huì)發(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)存布局如下:
沒有進(jìn)行空基類優(yōu)化。由于 MultiDerived1 是一個(gè)(is-a)Empty早芭,而且 MultiDerived2 也是一個(gè) Empty彼城,又由于 MultiDerived1 和 MultiDerived2 在子類的內(nèi)存空間中是連續(xù)的,此時(shí)如果進(jìn)行了空基類優(yōu)化退个,則兩個(gè) 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
我們分析一下這三個(gè)子類的內(nèi)存布局语盈,
MultiDerived4:
進(jìn)行了空基類優(yōu)化舱馅。由于 MultiDerived1 的 Empty 與子類成員 Empty 中間隔了 NotEmpty,所以不發(fā)生沖突刀荒,因此可以進(jìn)行優(yōu)化代嗤。
MultiDerived5:
沒有發(fā)生空基類優(yōu)化。因?yàn)?MultiDerived1 的 Empty 與子類成員 Empty 是連續(xù)的缠借,進(jìn)行優(yōu)化會(huì)發(fā)生沖突干毅。
MultiDerived6:
進(jìn)行了空基類優(yōu)化。因?yàn)?MultiDerived1 的 Empty 不會(huì)與其他 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 仍然進(jìn)行了空基類優(yōu)化,并沒有因?yàn)?Foo 中的成員 Empty 與 Derived2 的基類 Empty 相鄰而影響優(yōu)化绅喉,從“空基類優(yōu)化”這個(gè)名字也表明了該優(yōu)化只與繼承體系有關(guān)系渠鸽,而不考慮被優(yōu)化的類之外的干擾。
Foo1 也進(jìn)行了空基類優(yōu)化柴罐,但是比較特別拱绑,編譯器首先考慮的是將子類成員變量 Foo1Helper 進(jìn)行優(yōu)化(理由同 Foo1 中的 Derived2),此時(shí) Foo1Helper 內(nèi)存空間中已不存在 Empty丽蝎,所以也對(duì) Foo1 進(jìn)行了優(yōu)化猎拨。
Foo2 沒有發(fā)生空基類優(yōu)化膀藐,因?yàn)榈谝粋€(gè)成員 Foo 的第一個(gè)成員變量是 Empty,與基類中 Empty 發(fā)生了沖突红省。
結(jié)論
當(dāng)空類作為一個(gè)類的基類的時(shí)候额各,該空類占用的額外一個(gè)字節(jié)的內(nèi)存空間在子類中將會(huì)被優(yōu)化掉,除了一種情況外:在子類的內(nèi)存空間中有連續(xù)的相同類型的空類出現(xiàn)時(shí)(無論該空類是作為基類吧恃,超基類虾啦,子類的第一個(gè)非靜態(tài)成員變量,子類的第一個(gè)非靜態(tài)成員變量的基類痕寓,子類的第一個(gè)非靜態(tài)成員變量的成員傲醉,所有的這些都可以歸納為子類的內(nèi)存空間中基類的空間與接下來的第一個(gè)內(nèi)存塊),為了區(qū)分連續(xù)的空類呻率,將不進(jìn)行空基類優(yōu)化硬毕。
此外,在 C++11 中礼仗,空基類優(yōu)化是強(qiáng)制性的吐咳,不再是可選的。
空類的應(yīng)用
std::vector
在標(biāo)準(zhǔn)庫中元践,使用到分配器(allocator-aware)的類大多利用到了空基類優(yōu)化韭脊,進(jìn)而避免無狀態(tài)(stateless)的分配器成員占用額外的空間。
template <typename _Tp, typename _Alloc>
struct _Vector_base
{
typedef _Alloc<_Tp> _Tp_alloc_type; // 分配器的具體類型
typedef _Tp* pointer; // 存儲(chǔ)類型
// 數(shù)據(jù)存儲(chǔ)的具體實(shí)現(xiàn)
struct _Vector_impl : public _Tp_alloc_type
{
pointer _M_start; // 存儲(chǔ)的開始
pointer _M_finish; // 存儲(chǔ)的結(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)過簡(jiǎn)化的 std::vector 的代碼沪羔。我們需要關(guān)注的是 _Vector_base 中的 _Vector_impl。
對(duì)于 _Tp_alloc_type象浑,我們也可以不讓 _Vector_impl 繼承于 _Tp_alloc_type蔫饰,單獨(dú)設(shè)置一個(gè)成員變量,
template <typename _Tp, typename _Alloc>
struct _Vector_base
{
typedef _Alloc<_Tp> _Tp_alloc_type; // 分配器的具體類型
typedef _Tp* pointer; // 存儲(chǔ)類型
// _Vector_impl利用該變量進(jìn)行內(nèi)存的分配
_Tp_alloc_type _alloc;
// 數(shù)據(jù)存儲(chǔ)的具體實(shí)現(xiàn)
struct _Vector_impl
{
pointer _M_start; // 存儲(chǔ)的開始
pointer _M_finish; // 存儲(chǔ)的結(jié)束
pointer _M_end_of_storage; // 已經(jīng)分配的空間融柬,即capacity
};
};
由于無狀態(tài)的分配器是空類,沒有任何成員變量趋距,這樣處理的話會(huì)白白浪費(fèi)了一個(gè)字節(jié)的存儲(chǔ)空間粒氧,像std::vector 這樣的使用率非常高的類來說,代價(jià)非常高节腐。
所以標(biāo)準(zhǔn)庫采用了空基類優(yōu)化外盯,將分配器額外的存儲(chǔ)空間優(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)。當(dāng) _Cond 為 true 時(shí)狼渊,enable_if 進(jìn)行了部分模板特化箱熬,其中的 typedef 是關(guān)鍵类垦。
下面是 enable_if 的實(shí)例,
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值會(huì)展示成"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是雙精度浮點(diǎn)數(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
在上述的兩個(gè)例子中城须,_Cond 為 true 的模板特化中的 type 成為了關(guān)鍵蚤认,如果 _Cond 為 false,則使用type 會(huì)發(fā)生編譯錯(cuò)誤糕伐,因?yàn)樵谠椭袥]有 type砰琢。is_odd 利用的 type 作為返回值;is_even 則純粹是利用 type 作為編譯時(shí)的驗(yàn)證工具良瞧。
利用空類替代friend
關(guān)鍵字 friend 是一種強(qiáng)耦合陪汽,甚至強(qiáng)于繼承,所以我們應(yīng)當(dāng)小心地使用 friend 或者盡量避免褥蚯。
friend 的常見用途是訪問另一個(gè)類的私有構(gòu)造函數(shù)挚冤,
class Secret
{
friend class SecretFactory;
private:
// SecretFactory可以訪問該構(gòu)造函數(shù)
explicit Secret(std::string str) : _data{std::move(str)} {}
// SecretFactory同時(shí)也可以訪問該函數(shù),但是這可能會(huì)給我們?cè)斐陕闊? void addData(const std::string& moreData) { _data.append(moreData); }
private:
// SecretFactory無論如何也不應(yīng)該訪問該數(shù)據(jù)
std::string _data;
};
在上述例子中遵岩,SecretFactory 可以訪問不該訪問的 _data你辣,這會(huì)添加很多麻煩。
我們可以通過空類來限制 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是私有的,此時(shí)空類已經(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");
}
上例有兩點(diǎn)需要解釋誊锭,
對(duì)于①表悬,ConstructorKey 的構(gòu)造函數(shù)的訪問權(quán)限是 private,只有對(duì)其為 friend 的類才能訪問構(gòu)造函數(shù)丧靡;不能將構(gòu)造函數(shù)設(shè)置為 default蟆沫,即 ConstructorKey() = default;
,對(duì)于沒有非靜態(tài)成員的類(空類)來講温治,即使默認(rèn)構(gòu)造為 private饭庞,依然可以通過統(tǒng)一初始化方式(uniform initialization)對(duì)其進(jìn)行初始化,
class EmptyUniIni
{
EmptyUniIni() = default;
};
int main()
{
EmptyUniIni empty; // ERROE, 無法訪問構(gòu)造函數(shù)
EmptyUniIni empty1 {}; // OK, uniform initialization
}
對(duì)于②熬荆,需要將復(fù)制構(gòu)造函數(shù)設(shè)置為 private舟山,否則的話可以通過下面的代碼進(jìn)行構(gòu)造 Secret,
Secret::ConstructorKey* pk = nullptr;
Secret s { "Secret class", *pk };
這樣的話卤恳,我們前邊所做的努力就白費(fèi)了累盗。
std::input_iterator_tag, std::output_iterator_tag
// 用來標(biāo)記input iterator
struct input_iterator_tag { };
// 用來標(biāo)記output iterator
struct output_iterator_tag { };
// _Category即上述兩個(gè)標(biāo)簽
template <typename _Category, typename... _Others>
struct iterator { };
// 簡(jiǎn)略寫其他的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...>
{
};
上述是簡(jiǎn)化的 GCC 代碼。
由于 C++ 是強(qiáng)類型語言突琳,input_iterator_tag 和 output_iterator_tag 雖然什么都沒有若债,只有名字不同,他們也是不同的類型拆融,所以 istream_iterator 的父類和 ostream_iterator 的父類是不同的蠢琳,他們?cè)诶^承層次上沒有任何關(guān)系啊终,即 input_iterator_tag 標(biāo)記了 istream_iterator,output_iterator_tag 標(biāo)記了ostream_iterator挪凑。
而且上述代碼不會(huì)有任何性能上的缺陷孕索,因?yàn)榫幾g器會(huì)檢查模板中的參數(shù)是否被使用,如果沒有使用躏碳,則將該模板參數(shù)省略掉搞旭,進(jìn)而不會(huì)影響性能。
總結(jié)
通過上述講解菇绵,我們了解了空類的一些特性與應(yīng)用場(chǎng)景肄渗,利用空基類優(yōu)化或者與模板結(jié)合起來,會(huì)有奇妙的效果咬最。
參考
[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