摘要訊息 : 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 內部的 value0 初始化, 並且 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() constoperator 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.