C++20:核心语言
我上一篇文章 《C++ 20:四大巨头》 首先概述了概念(Concepts)、范围(Ranges)、协程(Coroutines)和模块化(Modules)。当然,C++ 20 还提供了更多的功能。今天,我们将继续讲述关于核心语言的概述。
核心语言
当你看到这张图时,你就明白我想要介绍的功能了。
三元比较运算符
三元比较运算符 通常被称为宇宙飞船运算符(spaceship operator)。该宇宙飞船运算符可用于确定两个值 A 和 B 的大小,是 AB。
编译器可以自动生成三元比较运算符。我们只需设置 default 就可以使用它了。在这种情况下,我们将得到全部共六个比较运算符,如 ==、!=、<、 和 >= 。
复制代码
#include 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); }
我认为,这个代码片段中最复杂的内容不是宇宙飞船运算符,而是使用了聚合初始化来初始化 Base。聚合初始化本质上意味着:如果所有成员都是公共的,我们可以直接初始化类类型(class、struct 或 union)的成员。在这种情况下,我们可以使用带括号的初始化列表,如上例所示。这只是一个简化示例。请阅读此处的详细信息: 聚合初始化 。
字符串文本作为模版参数
在 C++ 20 之前,我们不能使用字符串作为非类型模板参数。在 C++ 20 中,我们可以使用了。其思想是使用标准定义的 basic_fixed_string 类型, basic_fixed_string 具有一个 constexpr 构造函数。constexpr 构造函数允许它在编译时实例化固定的字符串。
复制代码
template class Foo { static constexpr char const* Name = T; public: void hello() const; }; int main() { Foo foo; foo.hello(); }
constexpr 虚拟函数
由于动态类型是未知的,因此无法在常量表达式中调用虚拟函数。C++ 20 将沿用这个限制。
指定初始化值
首先,让我先写一个聚合初始化的简单示例,如下所示:
复制代码
// aggregateInitialisation.cpp #include 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 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 T operator(T x) const { return x; }
有时,我们想定义一个仅适用于特定类型(如 std::vector )的 Lambda。此时,模板 Lambda 可以帮我们解决这个问题。除了类型参数,我们还可以使用概念( concept ):
复制代码
auto foo = [](std::vector 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 #include #include 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 中库的特性。