摘要訊息 : C++ 14 中一些容易忽略的新特性.
0. 前言
C++ 14 引入的重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 14 閱覽這些文章. 還有一些小特性也是 C++ 14 引入的, 本文文章將通過羅列的方式介紹這些特性.
更新紀錄 :
- 2022 年 6 月 4 日進行第一次更新和修正.
1. 聚合類別
C++ 11 要求在聚合類別的成員中, 非靜態成員變數不能使用函式呼叫或者指派的方法進行預設初始化; 否則, 這個類別就是聚合類別. C++ 14 提案 N3653《Member initializers and aggregates》提出了解除這個限制. 也就是說,
struct A {
int a = 0;
};
在 C++ 11 中並不是聚合類別, 而在 C++ 14 中就屬於聚合類別.
2. 帶有大小的 operator delete
C++ 11 允許給一個類別多載一個帶有大小的 operator delete
, 但是 C++ 11 又不允許多載全域名稱空間中帶有大小的 ::operator delete
. 有時候, 這可能會導致效能低下. 因為當帶有大小的多載 operator delete
不可用的時候, 需要呼叫全域的 ::operator delete
. 這個時候, 就需要主動去尋找需要回收的物件的大小. 因此, C++ 14 為標頭檔 <memory>
增加了 void operator delete(void * ptr, std::size_t sz) noexcept;
和 void operator delete[](void *ptr, std::size_t sz) noexcept;
.
3. 帶有記憶體配置的程式碼的優化
在 C++ 14 之前, 對於使用 new
進行記憶體的配置, 編碼器在處理這些程式碼的時候, 被 C++ 標準禁止優化. 例如
#include <vector>
int main(int argc, char *argv[]) {
for(auto i {0}; i < 128; ++i) {
for(auto j {0}; j < 256; ++j) {
std::vector<int> vec(j);
}
}
return 0;
}
這段程式碼中的 for
迴圈沒什麼實際用途, 因此兩個 for
迴圈應該被編碼器優化掉, 也就是直接刪除掉這段程式碼. 然而 std::vector
的建構子包含了使用 operator new
的記憶體配置, 而 C++ 11 禁止對此進行優化, 因此遵守 C++ 標準的編碼器是不能直接優化掉這段程式碼的. 為此, 程式效能可能就會受到影響. C++ 14 提案 N3664《Clarifying Memory Allocation》提出了解除這個限制.
4. 不盡如人意的型別轉換
在 C++ 11 下, 某些型別轉換是不盡如人意的, 考慮下列程式碼 :
#include <type_traits>
#include <cassert>
template <typename T, typename = typename std::enable_if<std::is_arithmetic<T>::value or std::is_pointer<T>::value>::type>
class zero_init {
private:
T value;
public:
zero_init() : value {static_cast<T>(0)} {}
zero_init(T value) : value {value} {}
operator T() const {
return this->value;
}
operator T &() {
return this->value;
}
};
zero_init<int *> p; // #1
assert(p == 0); // #2
p = new int(42); // #3
assert(*p == 42); // #4
delete p; // #5
delete (p + 0); // #6
delete +p; // #7
這裡我簡單解釋一下, 第二個樣板引數一般不是主動給出的. 當 T
是可計算的型別 (整型型別和浮點數型別) 或者指標型別的時候, 類別 zero_init
的物件才可以被宣告成功; 否則, 就會產生編碼錯誤. 也就是通過第二個樣板參數的預設引數, 限制了樣板可以接受的型別.
我們暫時不考慮 #6
和 #7
這兩行同時出現會導致的未定行為, 我們僅僅對語意進行分析. 對於 #1
, 樣板引數為 int *
, p
內部的 value
被 0
初始化, 並且 value
的型別為 int *
. 對於 #2
來說, 由於類別 zero_init
並沒有多載的比較運算子, 因此需要對 p
進行轉型, 剛好 p
對應的型別 zero_init<int *>
有一個多載的轉型運算子 operator int *&()
. 通過這個轉型運算子, 使得 p
可以和 0
進行比較. 對於 #3
, 編碼器會為 zero_init<T>
產生一個預設的複製指派運算子, 因此通過建構子 zero_init(int *)
建構一個臨時物件, 然後指派給 p
. #4
的行為和 #2
相同, 此處不再累贅. 但是 #5
則會產生編碼錯誤, 因為使用多載的轉型運算子 operator T() const
和 operator T &()
都可以, 編碼器無法判斷哪一個更好. #6
和 #7
最終都是呼叫 operator int *&()
.
C++ 14 提案 N3323《A Proposal to Tweak Certain C++ Contextual Conversions》主要解決了 #5
會產生編碼錯誤的問題. 在 C++ 14 中, 編碼器會優先呼叫 operator T &()
這個多載的運算子.
5. 二進制字面值常數
在 C++ 中, 我們用 0x
或者 0X
開頭的字面值常數表示十六進制數字, 使用 0
開頭的字面值常數表示八進制數字. 但是, 暫時還沒有某個方法可以表示二進制字面值常數. 因此 C++ 14 提案 N3472《Binary Literals in the C++ Core Language》提出了使用 0b
或者 0B
開頭的字面值常數表示二進制數字. 例如 0b101010
表示十進制數字 42
.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :