C++ 20 升级必备清单

Dori Exterman
Dori Exterman / 8月 02 2020
C++ 20 升级必备清单

C++ 20,你准备好了吗?

在上一个十年,C++ 给行业带来了很多惊喜。2020 伊始,C++20 的一系列突破性改变开启了新的十年。最新的C++20 增加了许多特性,且最终成功获得了最后一轮的批准,官方的版本有望在接下来的几个月中发布。

C++ 的升级蓄势待发,也给各个公司充分利用强化的语言优势,进一步升级代码库带来了绝佳的机会。然而,这个过程也充满了不确定和问题。标准的变更对公司业务会有帮助吗?升级的最佳方式是什么?迁移过程需要注意哪些陷阱?本文将解决大家的这些困惑。但是首先,我们先大致了解一下新版的 C++20.

开启 C++20

大约 10 年前,在经历一段较长时间 “小打小闹”地改进后,C++11 的“重拳出击”让这个语言向前迈进了一大步。C++11 带来了许多重要的新特性,例如移动语义(Move Semantics)和 Lambda 表达式(Lambda Expression)。C++14 和 C++17 也有一些重要的变更,但是由于变化不大,很多人并未进行升级。在总结前面的经验后,C++20 改善了之前版本的一些特性,同时也引入了一些新的特性。下面将列举这次升级的主要特性变化,或许能吸引大家进行更新。

模块

像 Java 和 C# 一样,C++20 提供了一个标准化的机制让代码可以重复使用,这个机制称为模块。模块功能将模块外部代码可以访问的类和函数明确导出,而保持所有其他代码为模块私有。其他的模块和代码文件继而可以导入模块,也可以导入提供的函数。

模块大幅地减少了对预处理器和头文件的依赖性。代码的安全性大大提升,摆脱了 #include 文件的命令等问题。模块只需进行一次而不是多次编译,让构建时间也得到精简。这在常规的头文件中也同样适用。当我们导入一个头文件时,它会像预编译的头文件一样运行,而无需进行 #include。

下面是一个简单的例子,说明如何导入、导出模块:

action.cppm
main.cpp
export module actions;

export class Action {
public:
  const char* perform() {
    return "done!";
  }
};
import actions;

import <iostream>;

int main() {
  std::cout << Action().perform() << '\n';
}

概念

很长时间以来,C++ 委员会都在努力加强对模板类和方法的约束,最终在新的C++20 中解决了这些问题。概念用于确保涉及给定类型的表达式能正确编译,而不仅仅只是显示复杂的 SFINAE。

与通常的模板相关错误相比,概念冲突引发的错误消息更容易理解和解决。尽早发现这些问题有助于确保应用程序能够像预期一样运作。概念还可以用于约束 Auto 接受的类型,从而实现对给定函数接受的类型施加更大的控制。

以下是使在 g++ 9.3.0 中通过概念约束函数接受类型的一个例子:

template<typename T>

concept Container = requires(T value) {

{ value.size() } -> std::size_t;

};

size_t size(Container& value) {
return value.size();
}

协程

C++20 引入了协程特性,让异步执行和惰性求值实行起来更为简单。协程可用于暂停函数的执行,将控制权交回调用者,且并不会因函数调用影响到当前的运行状态,之后可以从中止的位置开始恢复函数。有了协程支持,就无需进行回调。相反,我们可以在必要时挂起当前任务,转而对其他函数进行控制。

C++20 的协程与库的实现不同,如 Boost。与有栈的概念相对,协程、Boost.Fibers 都是无栈的。无栈协程每次调用都不需要使用独立的栈帧,而是重复使用调用方的栈帧,分配所需的数据以激活堆上的协程。在涉及到内存使用和上下文切换时,这样的效率要高得多。

范围

范围对处理数据集合的方式影响很大,因为它让用户可以轻松地借助管道过滤和转换数据。范围也使得迭代器配对变得无关紧要,减少了容易出错的代码。我们熟悉的“|”运算符,用于将数据从管道的一个部分串联到下一个部分,从而可以简单地利用一组常见的原始函数组成不同的管道。

下面是在 g++10 中使用范围对集合上的数据管道进行惰性求值的示例:

#include <iostream>

#include <ranges>

#include <vector>

int main() {
using std::views::filter,
std::views::transform,
std::views::reverse;

// Some data for us to work on
std::vector<int> numbers = { 6, 5, 4, 3, 2, 1 };

// Lambda function that will provide filtering
auto is_even = [](int n) { return n % 2 == 0; };

// Process our dataset
auto results = numbers | filter(is_even)
| transform([](int n){ return n++; })
| reverse;

// Use lazy evaluation to print out the results
for (auto v: results) {
std::cout << v << " ";   // Output: 2 4 6
}
}

一些小特性

C++ 20 新增了许多附加特性。例如,宇宙飞船运算符( Spaceship Operator )(即,<=>),它也可以是默认的,为大家提供了一个实现三路比较的函数。编译器可以通过这个函数自动生成其他风格的比较运算符。

C++ 20 改进了一些编译过程中的功能。传递非恒定参数时,Constexpr 函数会自动更改为在运行时执行,而新的 Consteval 关键字将其视为错误。String 和 Vector 也都可以在 Constexpr 函数中使用。Constexpr 函数现在可以是虚拟的,而且能使用 Try/Catch 语句块。

如何准备迁移?

C++20 切换是一个项重大的任务,工作量的大小取决于当前使用的 C++ 版本和代码库的大小。为了确保 C++20 正确编译,可能需要升级或更改依赖关系,所以对第三方的依赖性也会影响迁移过程。但一分耕耘一分收获,解决这些问题所做的努力最终将决定短期迁移所带来的效益。

对 C++20 中的新特性进行早期评估是很重要的,这有助于我们了解这些特性,并确定它们是否能满足使用需求。例如,如果我们目前使用的是协程库,对于 C++20 提供的本地解决方案也很感兴趣,我们可以编写一些测试代码评估 C++20 升级是否会有优势。如果等到做完了更改代码等所有工作之后才开始评估,最后发现这个功能无法按照需要的方式运行,那就为时已晚了。

在创建新的代码库之前,验证所有第三方依赖项是否能在升级的编译器中继续工作是很重要的。如果使用不同编译器,或尝试使用同一编译器的多个版本,那么标准库和 ABI 的更改可能会引发问题。先解决这些问题,有助于提前发现差异,让你离迁移成功更近一步。

以下是有助于准备 C++20 迁移的 5 个建议:

  • 尝试使用新的特性。建立代码库分支,并试验如何利用 C++20 的新特性增加价值。这将使您有机会了解这些特性的运作方式及局限性。
  • 保证第三方库的支持。除了编译代码,还需要保障第三方库能在新的 C++20 中继续使用。如果是从源代码中建立的这些库,也要确保升级后依然能正确构建。
  • 建立你的代码库。GCC、Clang 和微软 Visual C++ 都为 C++20 提供了不同程度的支持。在构建环境中,使用正确的编译标志位,例如 -std=c++2a,可以帮助构建成功。
  • 排除编译故障。在语言中引入新关键字、对标准库的更改以及弃用/删除的功能,都可能导致编译问题。请记住,在建议用户弃用某项功能时,不同编译器的警告程度有所不同。
  • 验证准确度和性能。为了确保一切都能正常工作,实际运行代码是很重要的。拥有自动化测试和 CI/CD 管道百利而无一害,也可以进行浸泡测试。

Hack your C++ build times with this free guide! Download free!

总结

C++20 对 C++ 语言标准的改进使得部署应用程序更快、更简单。在某些情况下,使用旧版代码库时,很难使用 C++20。这就是迁移的风险,可能是由于缺乏自动化测试而导致的。然而,在大多数情况下,迁移到 C++20 有助于扩展视野。

虽然本文关注的是 C++20 的主要变化,但是还有许多小范围的改进也将给大家带来更多的惊喜。 C++20 的许多小改变,比如引入了 Lambdas 模板,让代码简洁明练,又不失可读性。

开始使用 C++20 的最好方法是先尝试一些新的特性,并评估它们在特定环境下的实用性。在面临是否要升级的选择时,在实践中去使用 C++20 的经验能更好地帮助团队做出决策。随着 C++ 的飞速发展,保持主流——至少是主要版本,比如 C++20——是至关重要的。

Give your development a major speed boost

 

订阅博客

阅读 Incredibuild 独家内容

Dori Exterman

Dori Exterman 是一名软件开发专家、产品策略分析师,在软件开发行业拥有 20 年的工作经历。作为 Incredibuild 的首席技术官(CTO),他指导公司的产品策略,负责规划产品前景、执行方案、选择技术合作伙伴。在加入 Incredibuild 之前,Dori 在软件公司身兼数职,主要负责各种技术和产品开发,聚焦系统架构、产品性能、先端技术、DevOps、发布管理和 C++.他是开发工具先进技术领域的专家和分享者。