C++20

C++20，是繼C++17之後的C++程式語言的ISO/IEC標準修訂版的名稱。2020年2月，該標準在布拉格的會議上由WG21進行了技術定稿。同年9月4日草案獲得批准後，C++20同年12月正式發布。相比 C++17，C++20引入了新的語言特性，如概念、模塊、操作符、協程、指派初始化、新標準屬性等。C++20庫標準還加入了範圍、特性測試宏和位操作等。

## 特性改動

### 新增語言特性

#### 功能特性測試

自C++11之後標準引入了大量的C++語言、庫特性，在20標準前為了區分這些特性是否生效只能判斷C++標準。20標準為這些語言和程序庫的功能特性定義了一組預處理器宏，使之成為檢測這些功能特性是否存在的一種簡單且可移植的方式。測試宏展開會得到該語言、庫特性添加到標準草案中的年份和月份，如果該特性有顯著變更，宏展開的時間也為更新。

- 屬性測試宏

```cpp
__has_cpp_attribute( 属性记号 )
```

宏函數，用以檢測屬性是否支持，如：

```cpp
#if __has_cpp_attribute(nodiscard) > 201603L
#pragma message("nodiscard version is c++20")
#endif
```

- 語言特性測試宏

用以檢測當前某個語言功能特性是否支持，單個宏，如：

```cpp
#if __cpp_concepts >= 201907L
#pragma message("support concepts")
#endif
```

- 標準庫特性測試宏

用以檢測當前某個標準庫特性是否支持，單個宏，不由編譯器預定義，由頭文件定義：

```cpp
#ifdef __cpp_lib_bitops
#pragma message("support bitops")
#endif
```

#### 三路比較和比較操作符的默認

新增三路比較運算符，又稱spaceship operator，其形式為：

```cpp
左操作数 <=> 右操作数
```

表達式返回一個對象，使得

- 如果 a b) < 0
- 如果 a > b，那麼 (a b) > 0
- 而如果 a 和 b 相等/等價，那麼 (a b) == 0。

三路比較操作符會作為 >=四個操作符的重寫候選，若決議選擇了帶參數順序的operator，則對於操作如x @ y，執行 x y @ 0，對於不帶參數順序的執行 0 @ x y。

新增可以將比較操作符顯式預置=default來要求編譯器為某個類生成對應比較，比如：

```cpp
struct Point
{
    int x;
    int y;
    auto operator<=>(const Point&) const = default;
};
```

#### 聚合體指派初始化

聚合體初始化的語法糖，在c++11的聚合體初始化基礎上，增加了可以指派具體值的語法：

```cpp
struct U {
  int a;
  float b;
};

U u1{ 1, 2.0 };
U u2{ .a = 1, .b = 2.0 };
```

#### 範圍for中的初始化語句和初始化器

17標準中給if和switch語句加了初始化語句，20標準則給基於範圍的for加了初始化語句：

```cpp
std::initializer_list<int> il{ 1, 2, 3 };
for (size_t index{ 0 }; auto& i : il) {
  std::cout << std::format("index {} value is {}", index++, i) << std::endl;
}
```

#### UTF8字符基礎類型char8_t

新增了基礎類型char8_t用以表示UTF8字符，與11標準中的char16_t、char32_t一樣同為語言關鍵字，char8_t的出現主要是為了和舊有的char區分，專門用於表示utf8字符。相對應的標準庫``增加了std::u8string的別名，以下來自gcc13：

```cpp
#if __cplusplus >= 201703L && _GLIBCXX_USE_CXX11_ABI
#include <bits/memory_resource.h>
namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
  namespace pmr {
    template<typename _CharT, typename _Traits = char_traits<_CharT>>
      using basic_string = std::basic_string<_CharT, _Traits,
					     polymorphic_allocator<_CharT>>;
    using string    = basic_string<char>;
#ifdef _GLIBCXX_USE_CHAR8_T
    using u8string  = basic_string<char8_t>;
#endif
    using u16string = basic_string<char16_t>;
    using u32string = basic_string<char32_t>;
    using wstring   = basic_string<wchar_t>;
  } // namespace pmr
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace std
#endif // C++17
```

#### 新屬性

##### no_unique_address

該屬性適用於非位域非靜態數據成員，指示編譯器可以優化當前成員使其與其他非靜態數據成員重疊，減少內存占用。如果該成員為空類型（不具有數據成員），則編譯器優化為不占空間；如果該成員不為空，則其尾隨填充空間可被其他數據成員占用。兩個場景例子：

```cpp
struct Empty {};

struct WithEmpty {
  int32_t x;  // 4字节
  Empty e;  // 1字节（填充至对齐）
};  // 总大小8（4+1+3填充）

struct WithEmptyAttri {
  int32_t x;  // 4字节
  [[no_unique_address]] Empty e;  // 优化为不占空间
};  // 总大小4字节

struct NonEmpty {
  int32_t x;  // 4 字节
  [[no_unique_address]] char y;  // 1字节，无法优化，仍需对齐，*注意这里指定了no_unique_address
  // 3 字节填充
};  // 总大小8字节

struct WithNonEmpty {
  NonEmpty ne;  // 8 字节，未指定no_unique_address，z不可复用ne空间
  char z;  // 1字节, 需对齐
  // 3 字节填充
};  // 总大小12字节

struct Optimized {
  [[no_unique_address]] NonEmpty ne;  // 8 字节
  char z;  // 可以复用 ne 的尾随填充
};

int main() {
  static_assert(sizeof(WithEmpty) == 8);
  static_assert(sizeof(WithEmptyAttri) == 4);
  static_assert(sizeof(NonEmpty) == 8);
  static_assert(sizeof(WithNonEmpty) == 12);
  static_assert(sizeof(Optimized) == 8);
  WithNonEmpty wn;
  assert(&wn.z == &wn.ne.y + 4);
  Optimized o;
  assert(&o.z == &o.ne.y + 1);

  return 0;
}
```

##### likely與unlikely

現代cpu有指令預取和分支預測功能，在gcc以往也有__builtin_expect，相對應的20標準新增了likely和unlikely屬性作為c++標準的分支預測優化屬性，用於給程式設計師協助編譯器完成分支預測。

#### lambda更新

##### lambda顯式模板形參

可以顯式聲明模板形參用以表示當前為泛型lambda，如：

```cpp
auto print = []<typename T>(const T &t) { /*...*/ };
```

##### lambda捕獲參數包

```cpp
template <typename... Args>
auto FactoryByValue(Args&&... args) {
  return [... args = std::forward<Args>(args)]() {
    ((std::cout << "Value:" << args << " Address:" << &args << std::endl), ...);
  };
}

template <typename... Args>
auto FactoryByRef(Args&&... args) {
  return [&... args = std::forward<Args>(args)]() {
    ((std::cout << "Value:" << args << " Address:" << &args << std::endl), ...);
  };
}
```

以上代碼中分別按值和按引用捕獲了參數包，並通過一元右摺疊(c++17功能)打開了參數包。

##### 隱式按值捕獲this的棄用

20起須顯式聲明this的捕獲方式，如下代碼將被編譯器告警：

```cpp
struct A {
  void Test() {
    auto f = [=]() { cout << this << endl; };
  }
};
```

#### typename 關鍵字簡化

模板聲明中typename的使用被簡化，以下場景中無須再使用typename：

```cpp
struct Data {
  using ValueType = int32_t;
  using ValuePointer = int32_t*;
};

template <typename T>
struct Foo {
  using Type = T::ValueType;
  typedef T::ValuePointer Pointer;

  T::ValueType val_;

  T::ValueType Get();

  auto Get(size_t index) -> T::ValuePointer;

  template <typename U = T::ValuePointer>
  void Set(U u);
};
```

#### consteval與constinit

##### consteval

新增關鍵字consteval，只可用於修飾函數，consteval修飾的函數必須是編譯期執行：

```cpp
consteval int sqr(int x) {
  return x * x;
}
```

##### constinit

constinit關鍵字僅可用於變量，表明該變量擁有靜態初始化，即該變量的初值為編譯期常量，亦即const initialize，僅可用於靜態存儲期和線程存儲期的變量。

```cpp
const char* g() { return "动态初始化"; }
constexpr const char* f(bool p) { return p ? "常量初始化器" : g(); }
 
constinit const char* c = f(true);     // OK
// constinit const char* d = f(false); // 错误
```

#### constexpr 改動

- constepxr函數可以使用try catch，但異常不允許出現在編譯時：

```cpp
constexpr int Divide(int a, int b) {
    if (b == 0)
        throw std::runtime_error("Division by zero");
    return a / b;
}

constinit int i = Divide(1, 2); // 正常编译期常量
//constinit int j = Divide(1, 0); // 编译报错，编译期不允许出现异常
```

- 支持在constexpr函數內使用new、delete動態分配記憶體，如果是編譯期運行則記憶體不能流出編譯期之外：

```cpp
constexpr int* CreateArray(int size) {
    int* arr = new int[size];
    return arr;
}

constexpr void ReleaseArray(int* p) {
    delete[] p;
}

constexpr int Sum(int n) {
    int* p = CreateArray(n);
    std::iota(p, p + n, 1);  // constexpr since C++20
    auto t = std::accumulate(p, p + n, 0);  // constexpr since C++20
    ReleaseArray(p);
    return t;
}

constinit int i = Sum(10); // 正常编译期计算累计值55

// constinit int* p = CreateArray(100); // 编译出错，编译期内存不能出现在运行期
int* ap = CreateArray(100);  // constexpr函数可以是运行期函数
```

```cpp
class Parent {
public:
    constexpr virtual ~Parent() = default;

    constexpr virtual int GetID() {
        return 0;
    }
};

class Child : public Parent {
public:
    constexpr virtual ~Child() = default;

    constexpr int GetID() override {
        return 1;
    }
};

constinit int i = Parent().GetID();
constinit int j = Child().GetID();
```

```cpp
union Test {
    int i;
    int j;
};

constexpr int Foo()
{
    Test t{.i = 1};
    t.j = 2;

    return t.j;
}

constinit int i = Foo();
```

```cpp
#include <vector>
#include <string>
#include <algorithm>

constexpr int Foo() {
  std::vector<int> v;
  v.emplace_back(9);
  v.emplace_back(99);
  v.emplace_back(123);  // vector::emplace_back 20起为constexpr

  std::sort(v.begin(), v.end()); // std::sort 20起为constexpr

  // std::remove_if 20起为constexpr
  const auto erase_start =
    std::remove_if(v.begin(), v.end(), [](auto& i) { return i < 100; } );
  v.erase(erase_start, v.end()); // vector::erase 20起为constexpr

  std::string n = "Joshua";

  return v.size() + n.size(); // 两个size 20起都为constexpr
}

constinit int i = Foo();
```

#### 有符號整數必須以補碼表示

20標準起有符號整數必須以補碼表示，N位有符號整數的取值範圍也就固定為-(2N-1-1)到2N-1-1。

#### 移位運算符的行為變化

規範了左移、右移操作行為：

- 左移即是逐位左移並捨棄移出目標類型的位
- 右移即是算術右移

#### 允許使用圓括號進行聚合初始化

```cpp
#include <iostream>

struct U {
  int a;
  float b;
};

int main()
{
  U u(1,2);
  std::cout << u.a << " " << u.b << std::endl;
}
```

#### 縮略函數模板

函數模板可以省略`template<>`聲明進行簡寫：

```cpp
void f1(auto t) {} // => template<class T> void f1(T);

void f2(const auto& t, auto u) {} // => template<class T, class U> void f2(const T&, U)

void f3(std::integral auto) {} // => template<std::integral T> void f3(T)

void f4(std::integral auto&...) {} // => template<std::integral... Ts> void f4(Ts&...)
```

#### 陣列new 可推導數組長度

```cpp
int a[]{ 1, 2, 3, 4 };  // 11标准后编译通过
int* pa = new int[]{ 1, 2, 3, 4 }; // 20标准前无法通过编译，20后可以
```

標準規定在有列表初始化的情況下，可以通過列表個數來確定陣列大小。