空類(empty class)

空類就是沒有靜態(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容