原文详见: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 改进的文章,或者等待我的详细文章。无论如何,我们将获得的两个有趣的变化:
- 允许 [=, 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?C++14 标准中当我们写下一个泛型 lambda: [](auto x){ return x; } 时,编译器会自动生成一个带有模板化的调用操作符的类:
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 中的标准库特性。
网友评论