C++C++

C++20:核心语言

2020-02-22  本文已影响0人  奇点创客

原文详见:C++ 20: The Core Language

在上篇文章 C++20:四大件 中,我们对概念(concepts)、范围(ranges)、协程(coroutines)以及模块(modules)做了简要的介绍。当然,C++20 提供了很多东西。今天,让我们来继续了解一下核心语言(Core Language)部分。

核心语言(Core Language)

通过查看上图,便可知道我将要介绍的特性。

三路比较运算符 <=>

三路比较运算符 <=> 通常被戏称为飞船运算符。它用来确定两个值 A 和 B 到底是 A < B , A = B 还是 A > B。
你只需要礼貌地提出要求,编译器就可以自动生成三路比较运算符。在本例中,你将获得所有六个比较操作符,即 ==、!=、<、<=、> 和 >=。

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

操作符 <=> 默认执行字典序比较,它按照基类从左到右的顺序,并按字段声明顺序对非静态成员进行比较。下面是一个摘自微软博客的相当复杂的例子:Simplify Your Code with Rocket Science: C++ 20's Spaceship Operator

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);
}

我认为,这个代码片段中最复杂的部分不是飞船运算符,而是它使用了聚合初始化来初始化 Bases。聚合初始化也就意味着:如果所有成员都是公有的,那么你可以直接初始化类类型(类、结构或联合)的成员。在本例中使用了大括号初始化列表(braced-initialisation-list)来做这件事。关于聚合初始化的细节详见:聚合初始化

将字符串字面值作为模板参数

在 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 实例的参数是被显式声明的。该程序的输出与程序 aggregationInitialisation.cpp 的输出相同。注释 (1) (2) 所在的行非常有趣,行 (1) 会产生错误,因为指示符的顺序与其声明顺序不匹配。行 (2) 中 y 的指定值缺失。在这种情况下,y 将被初始化为 0,就如同使用大括号初始化列表 { 1, 0, 3 } 的效果一样。

Lambda 的各种改进

Lambda 表达式将在 C++20 中进行多项改进。
如果你想了解改进的详细信息,请转到 Bartek 的有关 C++17 和 C++20 中关于 lambda 改进的文章,或者等待我的详细文章。无论如何,我们将获得的两个有趣的变化:

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 中, 我们将不会在收到弃用警告。

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

但有时候,你想要定义一个仅适用于特定类型(如:std::vector)的 lambda 表达式。此时,模板 lambda 能帮我们达到这个目的。除了类型参数,你还可以使用一个概念:

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 中的标准库特性。

上一篇 下一篇

猜你喜欢

热点阅读