摘要訊息 : 一些 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) {
// ...
}
}
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :