What is empty class and why worth optimization
Empty class顧名思義就是空類,比如
class empty {};這裡empty顯然是一個空類,什麼成員都沒有。但是空類不限於這種形式,對於只有成員函數,並且沒有non-static data member的類,也可以是空類。
class empty{public: static void f(); void f();};class empty_too : public empty {};但是有一點需要注意,如果一個類或者他的基類中包含虛函數,那麼該類就不是empty class,因為通常一個含有虛函數的類,都有一個vptr,所以就有了資料成員,雖然是隱藏的。
對於父類是虛基類的情況也是一樣,因為一般子類需要一個指標指向虛基類的子物件。
對於一個空類,它的大小不是0,因為如果是0,那麼兩個對象就會擁有相同的地址,這樣就無法簡單用地址區分兩個對象了。通常一個空類的大小可能是1,也可能是4,這取決於編譯器。
如果一個類,它包含空類成員子物件,那麼這就會造成一定的空間浪費,而這個浪費是可以避免的,因為有些編譯器實現了一種稱為empty base class optimization的最佳化。
標準中提到了這種最佳化:
10/5 Derived classes
A base class subobject may be of zero size (clause 9); however, two subobjects that have the same class type and that belong to the same most derived object must not be allocated at the same address (5.10).
基類子物件可以是大小為0的,但是限制是,兩個相同類型的子物件不能分配在相同的地址上。所以技巧就是通過從空類來繼承是實現一定的最佳化。
class empty1 {};class empty2 {};class non_empty1{public: empty1 o1; empty2 o2;};class non_empty2 : public empty1 , public empty2{};這裡empty1,2是空類,通過繼承,non_empty2的大小還是1。但是non_empty1的大小就是2。
需要注意的是,繼承可能會帶來對介面的影響,因為在泛型代碼中,你不知道使用者傳入的類是否包含虛函數,如果包含虛函數,那麼可能有一個與我們的類正好同名的虛函數,這樣我們的函數就被虛化了。
解決這個問題可以不直接從空類繼承,而是建立一個中間類,並讓這種類來繼承空類,這樣可以將影響限制在我們的中間類中。並將這個中間類的對象作為成員儲存。
class empty {};class foo : public empty {}; // not always correctclass foo{ class bar : public empty {}; // ok, the interface of foo is not affected by the inheritance from empty;};在stl中,大量用到了函數對象,並且有許多函數對象是空的,如果大量儲存這些函數對象也是會造成一定的浪費的(為什麼要儲存?假設一下,哈哈)。
在《C++ template metaprogramming》中有這樣一個例子:有一個類,實現一個簡單的複合函數f(g(x))
template<typename R, typename F, typename G>class composed_fg{public: composed_fg(const F& f, const G& g) : f(f) , g(g) {} template<typename T> R operator ()(const T& t) const { return f(g(t)); }private: F f; G g;};這裡如果f或者g是空類,那麼就會造成空間的浪費,視編譯器而定,composed_fg最多可能會在32-bit平台上佔用8位元組。但是我們進行空基類最佳化,當f或者g中有空基類時,我們選擇不同的實現。
boost.compressed_pair就實現了一個最佳化過的std.pair,我們來分析一下boost.compressed_pair的實現。
compressed_pair根據T1, T2的類型,來選擇不同的實現,有6種情況
| T1 == T2 |
T1 empty |
T2 empty |
| false |
false |
false |
| false |
true |
false |
| false |
false |
true |
| false |
true |
true |
| true |
false |
false |
| true |
true |
true |
其中區分T1==T2是因為,C++不允許有2個相同的直接基類。
template <class T1, class T2>class compressed_pair_imp<T1, T2, 0>{public: typedef T1 first_type; typedef T2 second_type; typedef typename call_traits<first_type>::param_type first_param_type; typedef typename call_traits<second_type>::param_type second_param_type; typedef typename call_traits<first_type>::reference first_reference; typedef typename call_traits<second_type>::reference second_reference; typedef typename call_traits<first_type>::const_reference first_const_reference; typedef typename call_traits<second_type>::const_reference second_const_reference; compressed_pair_imp() {} compressed_pair_imp(first_param_type x, second_param_type y) : first_(x), second_(y) {} compressed_pair_imp(first_param_type x) : first_(x) {} compressed_pair_imp(second_param_type y) : second_(y) {} // ... void swap(::boost::compressed_pair<T1, T2>& y) { cp_swap(first_, y.first()); cp_swap(second_, y.second()); }private: first_type first_; second_type second_;};這個是T1和T2都不為空白的情況,這裡只是簡單地在對象中儲存了2個成員對象。再來看一下其中一個為空白的情況。
template <class T1, class T2>class compressed_pair_imp<T1, T2, 1> : protected ::boost::remove_cv<T1>::type{public: typedef T1 first_type; typedef T2 second_type; typedef typename call_traits<first_type>::param_type first_param_type; typedef typename call_traits<second_type>::param_type second_param_type; typedef typename call_traits<first_type>::reference first_reference; typedef typename call_traits<second_type>::reference second_reference; typedef typename call_traits<first_type>::const_reference first_const_reference; typedef typename call_traits<second_type>::const_reference second_const_reference; compressed_pair_imp() {} compressed_pair_imp(first_param_type x, second_param_type y) : first_type(x), second_(y) {} compressed_pair_imp(first_param_type x) : first_type(x) {} compressed_pair_imp(second_param_type y) : second_(y) {} void swap(::boost::compressed_pair<T1,T2>& y) { // no need to swap empty base class: cp_swap(second_, y.second()); }private: second_type second_;};這裡T1為空白,compressed_pair從T1繼承了,然而T2還是作為成員儲存起來了。還有一點變化就是swap中,只對second進行了操作,很顯然,因為T1子物件是空的,swap沒有意義。
其他的情況類似了,所以可以自己去看boost的源碼。
Side NoteVC中存在對Empty base class過度最佳化的情況,對於2個相同類型基類子物件的情況,在g++中,會產生2個位元組大小的對象,而VC中只是1個位元組的大小。
References[1] The "Empty Member" C++ Optimization
[2] Empty Base Class or Structure Assignment Operator May Corrupt Data
[3]
Understanding the Empty Base Optimization
[4] 《C++ template metaprogramming》
[5]
Why is the size of an empty class not zero?