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

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