C++20:核心語言

我上一篇文章 《C++ 20:四大巨頭》首先概述了概念(Concepts)、範圍(Ranges)、協程(Coroutines)和模塊化(Modules)。當然,C++ 20 還提供了更多的功能。今天,我們將繼續講述關於核心語言的概述。

核心語言

當你看到這張圖時,你就明白我想要介紹的功能了。

三元比較運算符<=>

三元比較運算符 <=> 通常被稱爲宇宙飛船運算符(spaceship operator)。該宇宙飛船運算符可用於確定兩個值 A 和 B 的大小,是 A<B、A=B 還是 A>B。

編譯器可以自動生成三元比較運算符。我們只需設置 default 就可以使用它了。在這種情況下,我們將得到全部共六個比較運算符,如 ==、!=、<、<=、> 和 >= 。

#include <compare>
struct MyInt {
  int value;
  MyInt(int value): value{value} { }
  auto operator<=>(const MyInt&) const = default;
};

默認運算符 <=> 可以執行字典序比較,它按照基類從左到右的順序,並按字段聲明順序對非靜態成員進行比較。下面是一個摘自微軟博客非常複雜的例子:用火箭科學簡化你的代碼:C++ 20 的宇宙飛船運算符

struct Basics {
  int i;
  char c;
  float f;
  double d;
  auto operator<=>(const Basics&) const = default;
};
 
struct Arrays {
  int ai[1];
  char ac[2];
  float af[3];
  double ad[2][2];
  auto operator<=>(const Arrays&) const = default;
};
 
struct Bases : Basics, Arrays {
  auto operator<=>(const Bases&) const = default;
};
 
int main() {
  constexpr Bases a = { { 0, 'c', 1.f, 1. },
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  constexpr Bases b = { { 0, 'c', 1.f, 1. },
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  static_assert(a == b);
  static_assert(!(a != b));
  static_assert(!(a < b));
  static_assert(a <= b);
  static_assert(!(a > b));
  static_assert(a >= b);
}

我認爲,這個代碼片段中最複雜的內容不是宇宙飛船運算符,而是使用了聚合初始化來初始化Base。聚合初始化本質上意味着:如果所有成員都是公共的,我們可以直接初始化類類型(class、struct 或 union)的成員。在這種情況下,我們可以使用帶括號的初始化列表,如上例所示。這只是一個簡化示例。請閱讀此處的詳細信息:聚合初始化

字符串文本作爲模版參數

在 C++ 20 之前,我們不能使用字符串作爲非類型模板參數。在 C++ 20 中,我們可以使用了。其思想是使用標準定義的 basic_fixed_string 類型, basic_fixed_string 具有一個 constexpr 構造函數。constexpr 構造函數允許它在編譯時實例化固定的字符串。

template<std::basic_fixed_string T>
class Foo {
    static constexpr char const* Name = T;
public:
    void hello() const;
};

int main() {
    Foo<"Hello!"> foo;
    foo.hello();
}

constexpr 虛擬函數

由於動態類型是未知的,因此無法在常量表達式中調用虛擬函數。C++ 20 將沿用這個限制。

指定初始化值

首先,讓我先寫一個聚合初始化的簡單示例,如下所示:

// aggregateInitialisation.cpp

#include <iostream>

struct Point2D{
    int x;
    int y;
};

class Point3D{
public:
    int x;
    int y;
    int z;
};

int main(){
    
    std::cout << std::endl;
    
    Point2D point2D {1, 2};
    Point3D point3D {1, 2, 3};

    std::cout << "point2D: " << point2D.x << " " << point2D.y << std::endl;
    std::cout << "point3D: " << point3D.x << " " << point3D.y << " " << point3D.z << std::endl;
    
    std::cout << std::endl;

}

我認爲沒有必要對這個程序進行解釋了。下面是這個程序的輸出:

顯式的聲明比隱式的要好。我們來看看這是什麼意思。在程序 aggregateInitialisation.cpp 中,初始化是非常容易出錯的,因爲我們可能在自己不注意的情況下變換構造函數參數的順序。下面所示的指定初始化值是從 C99 開始引入的。

// designatedInitializer.cpp

#include <iostream>

struct Point2D{
    int x;
    int y;
};

class Point3D{
public:
    int x;
    int y;
    int z;
};

int main(){
    
    std::cout << std::endl;
    
    Point2D point2D {.x = 1, .y = 2};
    // Point2D point2d {.y = 2, .x = 1};         // (1) error
    Point3D point3D {.x = 1, .y = 2, .z = 2};   
    // Point3D point3D {.x = 1, .z = 2}          // (2)  {1, 0, 2}
    

    std::cout << "point2D: " << point2D.x << " " << point2D.y << std::endl;
    std::cout << "point3D: " << point3D.x << " " << point3D.y << " " << point3D.z << std::endl;
    
    std::cout << std::endl;

}

Point2D 和 Point3D 實例的參數是被顯式聲明的。該程序的輸出與程序 aggregateInitialisation.cpp 的輸出相同。註釋掉的行(1)和(2)非常有趣。行(1)將會產生錯誤,因爲指定元素的順序與其聲明順序不匹配。 y 的指定值在行(2)中是缺失的。在這種情況下,y 將被初始化爲 0,類似於使用帶括號的初始化列表 {1、0、3} 對其進行初始化。

各種 Lambda 的改進

在 C++ 20 中, Lambda 將會有很多的改進。

如果你想了解更多變更詳細信息,請瀏覽 Bartek 發表的關於 C++ 17 和 C++ 20 中的 Lambda改進的文章,或者等待我編寫更詳細的文章。不管怎樣,在此,我們將介紹 Lambda 兩個有趣的變化。

  • **允許 [=, this] 作爲Lambda 捕獲器,並棄用隱式 this 捕獲器 [=] **
struct Lambda {
    auto foo() {
        return [=] { std::cout << s << std::endl; };
    }

    std::string s;
};

struct LambdaCpp20 {
    auto foo() {
        return [=, this] { std::cout << s << std::endl; };
    }

    std::string s;
};

在 C++ 20 中,隱式 [=] 捕獲器在 Lambda 結構中複製會引起一個棄用警告。當我們通過複製 [=, this] 顯式捕獲 this 對象時,在 C++ 20中, 我們將不會在收到棄用警告。

  • 模版 Lambda

你對模版 Lambda 的第一印象可能和我的一樣:我們爲什麼需要模版 Lambda ?當我們使用 C++ 14 中的 { return x; } 編寫一個泛型 Lambda 時,編譯器會自動生成一個帶有模板調用運算符的類:

template <typename T>
T operator(T x) const {
    return x;
}

有時,我們想定義一個僅適用於特定類型(如 std::vector )的 Lambda。此時,模板 Lambda可以幫我們解決這個問題。除了類型參數,我們還可以使用概念( concept ):

auto foo = []<typename T>(std::vector<T> const& vec) { 
        // do vector specific stuff
    };

新屬性:[[likely]] 和 [[unlikely]]

使用 C++ 20,我們可以獲取新的屬性 [[likely]] 和  [[unlikely]] 。不管執行路徑概率大小,這兩個屬性都允許它給優化器一個提示。

for(size_t i=0; i < v.size(); ++i){
  if (unlikely(v[i] < 0)) sum -= sqrt(-v[i]);
  else sum += sqrt(v[i]);
}

consteval 和 constinit 聲明符

新的聲明符 consteval 可以創建一個即時函數。對於即時函數來說,對該函數的每次調用都必須生成編譯時的常量表達式。即時函數是一個隱式的 constexpr 函數。

consteval int sqr(int n) {
  return n*n;
}
constexpr int r = sqr(100);  // OK
 
int x = 100;
int r2 = sqr(x);             // Error

由於 x 不是常數表達式,因此最終賦值時會導致錯誤 ,故 sqr(x) 無法在編譯時執行。

constinit 可以確保具有靜態存儲持續時間的變量在編譯時初始化。靜態存儲持續時間意味着在程序開始時分配對象,在程序結束時釋放對象。在命名空間作用域中聲明的對象(全局對象)、聲明爲 static 或 extern 對象都具有靜態存儲持續時間。

std::source_location

C++ 11 中有兩個宏 LINE  和  FILE ,它們可用於在使用宏時獲取信息。使用 C++ 20 ,source_location 類爲我們提供了源代碼的文件名、行號、列號和函數名等。下面是一個摘自 cppreference.com 的簡短示例,它展示了第一種用法:

#include <iostream>
#include <string_view>
#include <source_location>
 
void log(std::string_view message,
         const std::source_location& location = std::source_location::current())
{
    std::cout << "info:"
              << location.file_name() << ":"
              << location.line() << " "
              << message << '\n';
}
 
int main()
{
    log("Hello world!");  // info:main.cpp:15 Hello world!
}

接下來的安排?

這篇文章是有關核心語言中較小功能的第一篇概述。下一篇文章我將繼續講述 C++ 20 中庫的特性。

原文鏈接:

C++ 20: The Core Language

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章