C++ 20新特性

目录

Concepts

Ranges

Modules

Coroutines (协程)

Reflection

总结


前言

本文主体内容源自公众号 CSDN(ID:CSDNnews),作者:祁宇

本文将只评论大部分确定要加入和可能加入到 C++20 的重要特性,让读者对 C++ 的未来和演进趋势有一个基本的了解。这篇文章创作时间较早,所以一些内容和今年发布的 C++20 具体实现有些许不同。

C++ 20新特性_第1张图片

 

C++20 中可能增加哪些重要特性,下面这个图可以提供一个参考:

C++ 20新特性_第2张图片

下面是本文将评论的将进入和可能进入 C++20 的重要特性:Concepts、RangesModulesCoroutines 和 Reflection。

接下来让我们慢慢揭开 C++20 的面纱,看看这些特性到底是什么样的,它们解决了什么问题。

 

 

Concepts

在谈 Concepts 之前我想先介绍一下 Concepts 提出的背景和原因。众所周知,因为 C++ 的模版和模版元具备非常强大的泛型抽象能力并且是 zero overhead,所以模版在 C++ 中备受推崇,大获成功,在各种 C++ 库(如STL)中被广泛使用。

然而,模版编程还存在一些问题,比如有些模版的代码写起来比较困难,读起来比较难懂,尤其是编译出错的时候,那些糟糕的让人摸不着头脑的错误提示让人头疼。因此,C++ 之父 Bjarne Stroustrup 很早就希望对模版做一些改进,让 C++ 的模版编程变得简单好写,错误提示更明确。他早在 1987 年就开始做这方面的尝试了。

具体思路就是给模版参数加一些约束,这些约束相比之前的写法具有更强的表达能力和可读性,会简化 C++ 的泛型模版代码的编写。所以 Concepts 的出现主要是为了简化泛型编程,一个 Concept 就是一个编译期判断,用于约束模版参数,Concepts 则是这些编译期判断的合集。下面通过一个例子来展示 Concepts 是如何简化模版编程的:

template
class B {
public:
   template
   typename std::enable_if_t::value, std::string>
   to_string() const {
   return "Class B<>";
     }
};

B b1;                             // OK
std::cout << b1.to_string() << std::endl; // Compile ERROR!

B b2;                        // OK
std::cout << b2.to_string() << std::endl; // OK!

比如有这样一个类 B,我们调用它的成员函数 tostring 时,对 T 类型进行限定,即限定 T 类型是 std::string 的可转换类型,这样做的目的是为了更安全,能在编译期就能检查错误。这里通过 C++14 的 std::enable_if_t 来对 T 进行限定,但是长长的 enable_if_t 看起来比较冗长繁琐,头重脚轻。来看看用 Concepts 怎么写这个代码的:

template
    concept CastableToString = requires(T a) {
    { a } -> std::string;
};

template
class D {
public:
   std::string to_string() const requires CastableToString {
        return "Class D<>";
   }
};

可以看到,requires CastableToString 比之前长长的 enableift 要简洁不少,代码可读性也更好,CastableToString 就是一个 Concept,一个限定 T 为能被转换为 std::string 类型的 Concept,通过 requires 相连接,语义上也更明确了,而且这个 Concept 还可以复用。Concepts 的这个语法也可能在最终的 C++20 中有少许不同,有可能还会变得更简洁,现在语法有几个候选版本,还没最终投票确定。

 

 

Ranges

相比 STL,Ranges 是更高一层的抽象,Ranges 对 STL 做了改进,它是STL的下一代。为什么说 Ranges 是 STL 的未来?虽然 STL 在 C++ 中提供的容器和算法备受推崇和广泛被使用,但 STL一直存在两个问题: 

(1) STL 强制你必须传一个 begin 和 end 迭代器用来遍历一个容器;

(2) STL 算法不方便组合在一起。

STL 必须传迭代器,这个迭代器仅仅是辅助你完成遍历序列的技术细节,和我们的函数功能无关,大部分时候我们需要的是一个 range,代表的是一个比迭代器更高层的抽象。那么 Ranges 到底是什么呢?Ranges 是一个引用元素序列的对象,在概念上类似于一对迭代器。这意味着所有的 STL 容器都是 Ranges。在 Ranges 里我们不再传迭代器了,而是传 range。比如下面的代码:

STL写法:
std::vector v{1, 2};
std::sort(v.begin(), v.end());

Ranges写法:
std::sort(v);

STL 有时候不方便将一些算法组合在一起,来看一个例子:

std::vector v{1, 2, 3, 4, 5};

std::vector event_numbers;
std::copy_if(v.begin(), v.end(), std::back_inserter(event_numbers), [](int i){ return i % 2 == 0;});

std::vector results;
std::transform(event_numbers.begin(), event_numbers.end(), std::back_inserter(event_numbers), [](int i){ return i * 2;});

for(int n : results){
    std::cout<

上面这个例子希望得到 vector 中的偶数乘以 2 的结果,需求很简单,但是用 STL 写起来还是有些冗长繁琐,中间还定义了两个临时变量。如果用 Ranges 来实现这个需求,代码就会简单得多。

auto results = v | ranges::view::filter([](int i){ return i % 2 == 0; })
                 | ranges::view::transform([](int i){ return i * 2; });

用 Concetps 我们可以很方便地将算法组合在一起,写法更简单,语义更清晰,并且还可以实现延迟计算避免了中间的临时变量,性能也会更好。Concepts 从设计上改进了之前 STL 的两个问题,让我们的容器和算法变得更加简单好用,还容易组合。

 

 

Modules

一直以来 C++ 一直通过引用头文件方式使用库,而其他90年代以后的语言比如 Java、C#、Go 等语言都是通过 import 包的方式来使用库。现在 C++决 定改变这种情况了,在 C++20 中将引入 Modules,它和 Java、Go 等语言的包的概念是类似的,直接通过 import 包来使用库,再也看不到头文件了。

为什么 C++20 不再希望使用 #include 方式了?因为使用头文件方式存在不少问题,比如有 include 很多模版的头文件将大大增加编译时间,代码生成物也会变大。而且引用头文件方式不利于做一些 C++ 库和组件的管理工具,尤其是对于一些云环境和分布式环境下不方便管理,C++ 一直缺一个包管理工具,这也是 C++ 被吐槽得很多的地方,现在 C++20 Modules 将改变这一切。 

Modules 在程序中的结构如下图:

C++ 20新特性_第3张图片

上面的图中,每个方框表示一个翻译单元,存放在一个文件里并且可以被独立编译。每个 Module 由 Module 接口和实现组成,接口只有一份,实现可以有多份。 

Modules 接口和实现的语法:

export module module_name;
module module_name;

使用 Modules:

import module_name;

Modules 允许你导出类,函数,变量,常量和模版等等。接下来看一个使用 Modules 的例子:

import std.vector; // #include 
import std.string; // #include 
import std.iostream; // #include 
import std.iterator; // #include 
int main() {
     using namespace std;
     vector v = {
         "Socrates", "Plato", "Descartes", "Kant", "Bacon"
     };
     copy(begin(v), end(v), ostream_iterator(cout, "\n"));
}

可以看到不用再 include 了,直接去 import 需要用到的 Modules 即可,是不是有种似曾相识的感觉呢。曾看到一个人说如果 C++ 支持了 Modules 他就会从 Java 回归到 C++,也说明这个特性也是非常受关注和期待的。

 

 

Coroutines (协程)

很多语言提供了 Coroutine 机制,因为 Coroutine 可以大大简化异步网络程序的编写,现在 C++20 中也要加入协程了(乐观估计 C++20 加入,悲观估计在 C++23 中加入)。如果不用协程,写一个异步的网络程序是不那么容易的,以 boost.asio 的异步网络编程为例,我们需要注意的地方很多,比如异步事件完成的回调函数中需要保证调用对象仍然存在,如何构建异步回调链条等等,代码比较复杂,而且出了问题也不容易调试。而协程给我们提供了对异步编程优雅而高效的抽象,让异步编程变得简单!

C++ Courotines 中增加了三个新的关键字:co_await,co_yield  co_return,如果一个函数体中有这三个关键字之一就变成 Coroutine 了。co_await 用来挂起和恢复一个协程,co_return 用来返回协程的结果,co_yield 返回一个值并且挂起协程。

下面来看看如何使用它们,写一个 lazy sequence:

generator get_integers( int start=0, int step=1 ) {
    for (int current=start; current+= step)
        co_yield current;
}

for(auto n : get_integers(0, 5)){
    std::cout<

上面的例子每次调用 get_integers,只返回一个整数,然后协程挂起,下次调用再返回一个整数,因此这个序列不是即时生成的,而是延迟生成的。

接下来再看一下 co_wait 是如何简化异步网络程序的编写的:

char data[1024];
for (;;)
{
    std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), token);
    co_await async_write(socket, boost::asio::buffer(data, n), token);
}

这个例子仅仅用了四行代码就完成了异步的 echo,非常简洁!co_await 会在异步读完成之前挂起协程,在异步完成之后恢复协程继续执行,执行到 async_write 时又会挂起协程直到异步写完成,异步写完成之后继续异步读,如此循环。如果不用协程代码会比较繁琐,需要像这样写:

    void do_read()
	    {
		      auto self(shared_from_this());
		      socket_.async_read_some(boost::asio::buffer(data_, max_length),
				        [this, self](boost::system::error_code ec, std::size_t length)
				         {
				           if (!ec)
				           {
				             do_write(length);
				           }
				         });
		      }

    void do_write(std::size_t length)
	    {
		      auto self(shared_from_this());
		      boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
				        [this, self](boost::system::error_code ec, std::size_t /*length*/)
				        {
				          if (!ec)
				          {
				            do_read();
				          }
				        });
		    }

可以看到,不使用协程来写异步代码的话,需要构建异步的回调链,需要保持异步回调的安全性等等。而使用协程可以大大简化异步网络程序的编写。

 

 

Reflection

C++ 中一直缺少反射功能,其他很多语言如 Java、C# 都具备运行期反射功能。反射可以用来做很多事情:比如做对象的序列化,把对象序列化为 JSON、XML 等格式,以及 ORM 中的实体映射,还有 RPC 远程过程(方法)调用等,反射是应用程序中非常需要的基础功能。

现在 C++ 终于要提供反射功能了,C++20 中可会将反射作为实验库,最晚在 C++2中正式加入到标准中。

在反射还没有进入到C++标准之前,有很多人做了一些编译期反射的库,比如purecpp社区开源的序列化引擎 iguana,以及 ORM 库 ormpp,都是基于编译期反射实现的。然后,非语言层面支持的反射库存在种种不足之处,比如在实现上需要大量使用模版元和宏、不能访问私有成员等问题。

现在 C++ 终于要提供完备地编译期反射功能了,为什么是编译期反射而不是像其它语言一样提供运行期反射,因为 C++ 的一个重要设计哲学就是 zero-overhead,编译期反射效率远高于运行期反射。那么,通过 C++20 的编译期反射我们能得到什么呢?我们可以得到很多很多关于类型和对象的元信息,主要有:

(1) 获取对象类型或枚举类型的成员变量,成员函数的类型;

(2) 获取类型和成员的名称;

(3) 获取成员变量是静态的还是 constexpr;

(4) 获取方法是 virtual、public、protect 还是 private;

(5) 获取类型定义时的源代码所在的行和列。

 

所以 C++20 的反射其实是提供了一些可以编译期向编译器查询目标类型 "元数据" 的 API,下面来看看 C++20 的反射用法:

struct person{
    int id;
    std::string name;
};

using MetaPerson = reflexpr(person);
using Members = std::reflect::get_data_members_t;

using Metax = std::reflect::get_data_members_t;
constexpr bool is_public = std::reflect::is_public_v;

using Field0 = std::reflect::get_reflected_type_t;// int

上面的例子中,C++20 新增关键字 reflexpr 返回的是 person 的元数据类型,接下来我们就可以查询这个元数据类型了, std::reflect::getdatamembers_t 返回的是对象成员的元数据序列,我们可以像访问 tuple 一样访问这个序列,得到某一个字段的元数据之后我们就可以获取它的具体信息了,比如它的具体类型是什么,它的字段名是什么,它是公有还是私有的等等。

注意:C++20 的反射语法还没有最终确定,这只是一种候选的语法实现,还有一种没有元编程的语法版本,该版本通过编译期容器和字符串来存放元数据,比如 constexpr std::vector,constexpr std::map,constexpr std::string 等 ,这样就可以像普通的 C++ 程序那样来操作元数据了,用起来可能更简单。

C++20 的编译期反射实际上提供了一些编译期查询 AST 信息的接口,功能完备而强大。

 

总结

Concepts 让 C++ 的模版程序的编写变得更简单和容易理解;

Ranges 让我们使用 STL 容器和算法更加简单,并且更容易组合算法及延迟计算;

Modules 帮助我们大大加快编译速度,同时弥补了 C++ 使用库和缺乏包管理的缺陷;

Coroutines 帮助我们简化异步程序的编写;

Reflection 给我们提供强大的编译期 AST 元数据查询能力;

总而言之,C++ 的新标准都是为了让 C++ 变得更简单、更完善、更强大、更易学和使用,这也是 C++ 之父希望未来 C++ 演进的一个方向和目标。C++20,一言以蔽之:Newer is Better! 

你可能感兴趣的:(C/C++)