摘要訊息 : 一些 C++ 20 引入的小特性.

0. 前言

C++ 20 引入的絕大多數重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 20 閱覽這些文章. 還有一些小特性也是 C++ 20 引入的, 本文文章將通過羅列的方式介紹一部分特性.

1. 位元欄位語法增強

在位元欄位的宣告中, 如果和條件運算子結合, 會得到一些比較有趣的情形 :

int a;
const int b {0};
struct S {
    int x : true ? 8 : a = 42;      // OK, 宣告 int x : 8 並且指派 42 給 a
    int y : true ? 8 : b = 42;      // 編碼錯誤, 因為 b 不能被修改
    int z : 1 || new int {0};       // OK, 宣告 int z : 1
};

C++ 20 提案 P0683R1《Default member initializers for bit-fields》提出, 如果像用類似的方式給位元欄位進行初始化, 那麼應該給模稜兩可的部分增加括號 :

int a;
const int b {0};
struct S {
    int x : true ? 8 : a = 42;      // OK, 宣告 int x : 8 並且指派 42 給 a
    int y : (true ? 8 : b) = 42;    // OK, y 被預設初始化為 42
    int z : (1 || new int) {0};     // OK, z 被預設初始化為 0
};

2. 成員函式的呼叫

struct X {
    void f() const &;
};
int main(int argc, char *argv[]) {
    X {}.f();		// OK
    (X {}.*&X::f)();		// Error
}

Code 2 中, 產生編碼錯誤的那一行本不應該產生錯誤. 因為 X 的成員函式 f 被一個帶有 const 的左值參考所標識, 這意味著它既可以被左值所呼叫, 又可以被右值所呼叫. 而如果採用指向成員函式的指標來呼叫, 就會產生編碼錯誤, 這個錯誤顯然不符合預期. 因此, C++ 20 提案 P0704R1《Fixing const-qualified pointers to members》提出了修正這個問題, 現在 Code 2 可以通過編碼.

3. __VA_OPT__

可變參數樣板中, 可變的那一部分完全可以是空的. 例如 F<Args...> 對應的 F<> 同樣合法. 但是對於可變參數巨集來說就不是這樣了, 它的可變部分至少需要提供一個引數 :

#define F(X, ...) f(42, X, __VA_ARGS__)
#define G(X) f(42, X)
#define H(...) f(42, __VA_ARGS__)

constexpr int a {};

template <typename ...Args>
int f(int, Args...);

int x {F(a, 0)};		// OK, same as f(42, a, 0)
int y {F(a)};		// Error, same as f(42, a, )
int z {H()};		// Error, same as f(42, )

#ifdef M
int iz = F(a, M);		// Error when M is an empty macro (#define M).
#else
int iz = G(a);
#endif

為了解決這個問題, C++ 20 提案 P0306R4《Comma omission and comma deletion》為 C++ 引入了一個新的巨集 __VA_OPT__, 它表示某些參數是可選的 :

#define F(X, ...) f(42, X __VA_OPT__(,) __VA_ARGS__)
#define G(X) f(42, X)
#define H(...) f(42 __VA_OPT__(,) __VA_ARGS__)

constexpr int a {};

template <typename ...Args>
int f(int, Args...);

int x {F(a, 0)};		// OK, same as f(42, a, 0)
int y {F(a)};		// OK, same as f(42, a)
int z {H()};		// OK, same as f(42)

#ifdef M
int iz = F(a, M);		// no problem when M is empty macro (#define M)
#else
int iz = G(a);
#endif

不過, 預處理運算子 ## 不支援出現在 __VA_OPT__ 的開頭位置 :

#define CONCAT(X, ...) X __VA_OPT__(## __VA_ARGS__)		// Error
#define CONCAT(X, Y, ...) X##Y __VA_OPT__(,) __VA_ARGS__		// OK

C++ 20 的另一個提案 P1042R1__VA_OPT__ wording clarifications》__VA_OPT__ 引入了空參數的支援. 也就是說, 若 __VA_OPT__(X) 中的 X 沒有被提供, 那麼 __VA_OPT__(X) 會被一個空的符號所替換. 另外, __VA_OPT__() 本身也會被空的符號所替換 :

#define M(X, ...) __VA_OPT__(X##X)
#define N(X, ...) __VA_OPT__()##X##__VA_OPT__() __VA_OPT__(,) __VA_ARGS__

M(, 1)		// OK
int N(x);		// OK, declare int x;

4. 指定初始化

C++ 20 提案 P0329R0《Designated Initialization》為 C++ 引入了 C99 中的指定初始化語法 :

struct X {
    int a;
    int b;
    int c;
};

X a = {.a = 1, .b  = 2, .c = 3};		// same as X a {1, 2, 3};
X b = {.a = 1};		// same as X b {.a = 1, .b = 0, .c = 0};

不過, C99 中的指定初始化比較寬鬆, 而 C++ 中的比較嚴格. C++ 中的指定初始化不支援下面場景 :

  • 指定初始化列表中的初始化順序必須嚴格按照成員變數的宣告順序, 即 X c {.b = 1, .a = 2}; 是不允許的;
  • 省略的情形只能發生在尾部, 省略掉的成員會被預設初始化, 即 X c {3, .b = 3}; 是不允許的;
  • 不支援對陣列進行初始化.

另外, C++ 也不支援巢狀的指定初始化 :

struct X {
    int a;
    int b;
};
struct Y {
    X x;
    int y;
};
Y y {.x.a = 1, .x.b = 0, .y = 1};       // Error
Y z {{.a = 1, .b = 2}, .y = 1};     // OK

但是實際上經過測試, Code 7 在 Clang 下是可以通過編碼的. 這可能是從 C 和 C++ 相容性上考慮而作出的讓步.

5. 帶有初始化的 Range-For

C++ 17 為條件陳述式增加了初始化的空間 (《【C++ 17】帶有初始化的條件陳述式》), 現在 Range-For 也可以這樣做了 :

#include <map>
#include <string>

{
    std::map<int, std::string> m {...};
    for(auto &p : m) {
        // ...
    }
}

{
    for(std::map<int, std::string> m {...}; auto &p : m) {
        // ...
    }
}