摘要訊息 : 一些 C++ 17 引入的小特性.
0. 前言
C++ 17 引入的絕大多數重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 17 閱覽這些文章. 還有一些小特性也是 C++ 17 引入的, 本文文章將通過羅列的方式介紹一部分特性.
更新紀錄 :
1. 聚合初始化
所謂聚合初始化, 就是對聚合體進行初始化, 到 C++ 11 為止, 這個聚合體包含了 :
- 陣列;
- 複合下面要求的類別 (通常為結構體或者等位) :
- 沒有用戶自訂的建構子;
- 不存在非靜態私用成員;
- 成員不可被預設初始化;
- 沒有虛擬函式;
- 沒有從基礎類別繼承.
由於這些型別聚合了多個變數, 因此被稱為聚合體 (aggregate). 聚合初始化就是按照一個聚合體中變數宣告的順序, 使用初始化列表對這些變數按宣告順序進行初始化. 不過需要注意的是, 和列表初始化相似, 窄化型別轉化是不被允許的. 上面提到, 聚合體可以包含靜態成員變數, 也可以包含不具名實體, 但是在聚合初始化的時候, 會跳過這些靜態成員和不具名實體, 只按照普通成員變數的順序進行初始化. 如果初始化列表中的引數數量並不足以初始化聚合體中全部的變數, 那麼剩下來的變數會被預設初始化; 但是如果初始化列表中的引數數量超出了一個聚合體中變數的數量, 就會產生編碼錯誤. 最後, 使用聚合初始化的方式對等位進行初始化的時候, 只會初始化第一個非靜態成員變數.
從上面針對聚合體的定義中, 某一些要求其實可以被簡化, 使得聚合初始化可以應用到更多的場景中. C++ 17 提案 P0017R1《Extension to aggregate initialization》就提出解除聚合初始化在非私用基礎類別上的限制. 在建構子的匹配上, 如果建構子沒有被 explicit
標識, 那麼聚合初始化裡的引數會被當作普通的函數風格的初始化, 和用戶自訂的建構子進行匹配. 但是, 如果出現建構子繼承的情況, 聚合初始化就不再可用. 而 P0017R1 就提出了讓聚合初始化按照繼承的順序, 在 public
繼承情況下仍然可用 :
Code 1. 聚合初始化struct S { int a; S(int a, int) : a {a} {} }; struct S2 : S { char b; }; struct S3 : S2 {}; struct base1 { int a; }; struct base2 { char b {'0'}; }; struct base3 { float c {0.1f}; }; struct derived : base1, base2, base3 { const char *str; }; int main(int argc, char *argv[]) { S2 a {{1, 2}, 0}; // {1, 2} 匹配 S(int, int); 第二個 0 去初始化 S2 中的成員變數 b S3 b {{{0, 0}}}; // {0, 0} 匹配 S(int, int); {{0, 0}} 用於初始化 S2, S2 中的 b 被預設初始化; {{{0, 0}}} 用於初始化 S3 derived d {{42}, {}, {0.3f}, "hello world!"}; // {42} 用於初始化 base1 中的 a, base2 中的 b 被預設初始化, {0.3f} 用於初始化 base3 中的 c, 而 "hello, world!" 用於初始化 derived 類別中的 str }
2. 表達式計算順序
我們曾不止一次指出 C++ 的表達式計算順序問題, 特別是類似於 function_call(expr1, expr2, expr3)
, 其中的 expr1
, expr2
和 expr3
的計算順序是實作定義行為, 這也就導致了某些情況下我們需要注意這些問題, 以避免資源流失的情況出現. 不過, 這種問題一直是 C++ 討論的重點, 從 C++ 誕生起就有. 然而, 問題不只是被討論這麼簡單, 就算是 C++ 老手, 也可能會被這樣的問題所困擾, 因為不同編碼器產生的結果並不一定相同, 於是就有了下面這個被寫入 C++ 最權威教科書 ("The C++ Programming Language" 4th.) 的錯誤實例 :
Code 2. 表達式計算順序導致的錯誤std::string s = "but I have heard it works even if you don’t believe in it"; s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, ""); assert(s == "I have heard it works only if you believe in it");
上面這個程式碼會不會因為斷言失敗而產生運作時錯誤呢? 在 C++ 17 之前, GCC 和 MSVC 這兩個編碼器都可能產生錯誤. 這是因為上面這段程式碼存在實作定義行為, C++ 標準並沒有嚴格規範計算順序, 從而使得在不同編碼器之下的行為有所不同. 因此, C++ 17 提案 P0145r3《Refining Expression Evaluation Order for Idiomatic C++》提出對 C++ 三十多年來沒有怎麼修改過的表達式計算順序有一個更嚴格的限制 : 對於陣列注標運算, 函式呼叫, 成員訪問, 字尾遞增, 字尾遞減, 位元左移和位元右移, 限制它們的計算順序為從左到右; 對於運算指派 ("+=
", "-=
", "*=
", "/=
", "%=
", "&=
", "|=
", "^=
", "<<=
" 和 ">>=
") 和直接指派, 限制它們的計算順序為從右至左. 這樣, 上面提到的問題自然就被解決了.
Tip : 對於本節中提到的
std::string s = "but I have heard it works even if you don’t believe in it";
計算順序問題, 大家可以參考這個帖子 : https://stackoverflow.com/questions/27158812/does-this-code-from-the-c-programming-language-4th-edition-section-36-3-6-ha.
3. if constexpr
在 C++ 17 之前, 如果我們要在編碼器就決定是否運作函式的某個部分, 那麼就要借助樣板 :
Code 3-1. C++ 17 之前的編碼器抉擇template <bool> void f_block(...) { //... } template <> void f_block<false>(...) { //... } void f() { //... f_block<false>(); //... f_block<true>(); //... }
由於函式樣板無法偏特製化, 這也就導致了這些函式的參數型別不能是泛型的. 如果我們確實需要泛型, 那麼需要借助類別樣板才可以做到. 為了降低語法的複雜性, C++ 17 提案 P0292R2《constexpr if: A slightly different syntax》引入了 if constexpr
這樣的語法. 其使用方法類似於 if
陳述式, 條件必須是編碼期可計算的常數表達式. 如果判斷表達式的計算為 true
, 那麼編碼期就會將接下來的區塊納入需要運作的範圍 :
Code 3-2. if constexprvoid f() { //... if constexpr(expr) { // 如果 expr 為 true, 那麼這個區塊的程式碼將被運作; 否則, 會跳過這段程式碼 } //... }
不過例外的是, 即時條件判斷最終的計算結果為 false
, 如果區塊內部存在 static_assert
, 且斷言的結果為 false
, 那麼同樣會導致編碼錯誤 :
Code 4. if constexpr 中的 static_assertvoid f() { //... if constexpr(false) { //... static_assert(false); // Error //... } //... }
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :