C++20相关

concept关键字

当我们把一个类型带入模板中,如果没有提供模板要求的函数或运算符,模板就不能转换为实际可编译的函数,从而产生大量错误信息。

concept定义

template <typename MyType>
concept Any = true;

我们定义了一个概念,它永远为真,也就是说,任何类型都能满足此概念。同时,concept作为一个bool编译期常量完全可以用bool常量的方式使用:

std::cout << std::boolalpha;
std::cout << Any<int> << std::endl;

这段代码里,我用刚刚定义的概念Any,代替typename的位置,对模板参数AnyType提供了一个约束,虽然只是约束了一个寂寞:

template <Any AnyType>
void printSize(const AnyType& anyArg)
{
    std::cout << sizeof(AnyType) << std::endl;
}

使用该模板函数时,编译器会自动把后面的AnyType带入到Any的第一个参数中,只有值为true时才使用这个模板,否则编译器会寻找其他满足约束的模板,直至报错。 STL库的concepts头文件中有以下3个例子:

template<typename _Tp>
concept integral = is_integral_v<_Tp>;

template<typename _Tp>
concept signed_integral = integral<_Tp> && is_signed_v<_Tp>;

template<typename _Tp>
concept unsigned_integral = integral<_Tp> && !signed_integral<_Tp>;

requires表达式

以数学上的域和线性空间为例:

template <typename ScalarType>
concept Field = requires(ScalarType x, ScalarType y)
{
    {x + y} -> std::same_as<ScalarType>;
    {x - y} -> std::same_as<ScalarType>; 
    {+x} -> std::same_as<ScalarType>;
    {-x} -> std::same_as<ScalarType>;
    ScalarType(0);

    {x * y} -> std::same_as<ScalarType>;
    {x / y} -> std::same_as<ScalarType>;
    ScalarType(1);
};

template <typename VectorType, typename ScalarType>
concept LinearSpace = requires(VectorType x, VectorType y, ScalarType k)  
{
    requires Field<ScalarType>;

    {x + y} -> std::same_as<VectorType>;
    {x - y} -> std::same_as<VectorType>; 
    {+x} -> std::same_as<VectorType>;
    {-x} -> std::same_as<VectorType>;
    VectorType(0);

    {k * x} -> std::same_as<VectorType>;
    {x * k} -> std::same_as<VectorType>;
    {x / k} -> std::same_as<VectorType>;
};

requires表达式同样是bool常量,所以它能直接赋值给concept。

requires表达式,当然要用requires开头,后面的小括号里面是在大括号中的表达式中会用到的变量的声明。requires语句的参数并不会实例化,所以不必担心这个参数的类型没有默认构造函数,或者类型是抽象类的问题,更不必担心我是不是要为了避免多余的拷贝使用引用或右值引用的问题。

大括号中的语句分为4类:

  1. 简单语句 VectorType(0);,它是一个表达式用分号结束,如果这个语句能够通过编译,那么其值就为true,否则就是false。
  2. 类型检查语句 {x + y}-> std::same_as<VectorType>;,如果大括号中的表达式可以通过编译,而且其结果满足->后面的concept,结果才为true。 这里我们用到了concepts头文件中提供的std::same_as概念,它的作用是检查约束的类型是否与指定的类型相同,其定义为:
template<typename _Tp, typename _Up>
concept same_as = std::is_same_v<_Tp, _Up>;
  1. requires子句requires Field<ScalarType>;,这句话的意思就是对于线性空间其标量类型必须是一个域,其语法为requires /*bool常量表达式*/;
  2. 这里还有一种没有用到的对类型的要求,语法为typename /*要求存在的类型*/;,只有这个类型真的存在时,其结果才是true,当然实际使用时,这个类型当然得和concept中约束的类型有关系,要么得是嵌套的类型,要么得是以此类型为参数的模板。

这种用法对数学家是挺好的,对程序员却不友好,因为约束越多,requires后面的小括号中的参数就会极速膨胀,而且把每个语句的参数都写到一起,可读性也非常差。能想到的最好看的写法就是:

template <typename MyType>
concept HasName = 
(
    requires(const MyType myObj)
    {
        {myObj.GetName()} -> std::same_as<std::string>;
    }

    && requires(MyType myObj, const std::string& name)
    {
        {myObj.SetName(name)} -> std::same_as<void>;
    }
);

因此更推荐使用requires子句:

template <typename MyType>
concept ToStringAble = requires
{
    requires requires(const MyType myObj)
    {
        {myObj.ToString()} -> std::same_as<std::string>;
    };
};

协程(coroutine)

协程就是一个可以 挂起(suspend)恢复(resume) 的函数(但无论如何不能是 main 函数)。你可以暂停协程的执行,去做其他事情,然后在适当的时候恢复到暂停的位置继续执行。协程让我们使用同步方式写异步代码。

hello.cpp演示协程

// coro_task.h
#ifndef COROUTINE_CORO_TASK_H
#define COROUTINE_CORO_TASK_H

#include <coroutine>
#include <exception>
#include <type_traits>

class Task
{
public:
    struct promise_type;
    using TaskHd1 = std::coroutine_handle<promise_type>;

private:
    TaskHd1  hd1_;

public:
    Task(auto h) : hd1_{h} {}
    ~Task() {
        if (hd1_) { hd1_.destroy(); }
    }

    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;

    bool resume() {
        if (!hd1_ || hd1_.done())
            return false;
        hd1_.resume();
        return true;
    }

public:
    struct promise_type {
        /* data */
        auto get_return_object() {
            return Task{TaskHd1::from_promise(*this)};
        }
        auto initial_suspend() { return std::suspend_always{};}
        void unhandled_exception() { std::terminate();}
        void return_void() {}
        auto final_suspend() noexcept { return std::suspend_always{}; }
    };
};
#endif //COROUTINE_CORO_TASK_H

// main.cpp
#include <iostream>
#include "coro_task.h"

Task hello(int max) {
    std::cout << "hello world\n";
    for (int i = 0; i < max; ++i) {
        std::cout << "hello " << i << "\n";
        co_await std::suspend_always{};
    }

    std::cout << "hello end\n";
}

int main() {
    auto co = hello(3);
    while (co.resume()) {
        std::cout << "hello coroutine suspend\n";
    }
    return 0;
}

从总体上来看,协程和调用者之间的关系可由下图表示

若要实现一个协程,需要首先提供一个协程接口,譬如Task,在协程接口中需要提供:

  • promise_type
  • std::coroutine_handle<promise_type> 在协程的函数体中需要使用 co_await,co_yield, co_return 之一的关键词。

hello.cpp协程工作过程

  1. 协程的调用同函数相同,在main函数中,通过如下形式调用协程: auto co = hello(3);
  2. 通过调用hello(3), 启动协程,协程立即暂停(suspend), 并返回协程接口Task对象给调用者
  3. 在main函数中,调用co实例的resume接口,通过coroutine handle恢复协程的执行
  4. 在协程中,进入for循环,初始化局部变量,并到达暂停点(suspend point), 暂停点由co_await expr确定
  5. 协程暂停后将控制权转移到main函数,main函数继续运行并从新恢复协程
  6. 协程恢复执行,继续for循环,i的值增加。再一次到达暂停点(suspend point)。转换控制权到main函数。
  7. 最终for循环结束,协程离开for循环。并将控制权返回到到main函数,main函数退出循环,并销毁协程。

promise_type

promise_type可以用来控制协程的行为。详细来说,promise_type给开发者提供了如下能力:

  • 与 coroutine 交互(从coroutine接受消息,以及将消息发送给coroutine)
  • 创建 coroutine_handle
  • 控制 coroutine 的 suspend 时机(在coroutine的入口和结尾)
  • 提供了异常处理

promise_type 共有以下这些接口:

struct promise_type
{
    coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
    auto yeid_value(val);
    auto return_value(val);
    auto await_transform(...);
    operator new(sz);
    operator delete(ptr, sz);
    auto get_return_object_on_allocation_failure();
};

一个类(模版类)只要实现了promise type所需要的接口,那么该类便可以用作协程的promise_type。一个简单的模版promise_type类如下:

#include <coroutine>
#include <exception>

template <typename Type>
struct CoroPromise {
  auto get_return_object() {
      return std::coroutine_handle<CoroPromise<Type>>::from_promise(*this);
  }

  auto initial_suspend() { return std::suspend_always{}; }

  void unhandled_exception() { std::terminate(); }

  void return_void() {}

  auto final_suspend() noexcept { return std::suspend_always{}; }
};

接口说明如下:

  1. get_return_object(): 主要用来初始化协程接口,用来作为协程的返回值。 该函数一般做两件事情
    • 创建coroutine handle. 通过std::coroutine_handle的静态成员函数from_promise创建和初始化
    • 利用创建的coroutine handle初始化协程接口
  2. initial_suspend(): 定制coroutine启动时的行为。 eagerly 或者 lazily。
    • 当返回值为std::suspend_never{},coroutine 立马执行(eagerly)
    • 当返回值为std::suspend_always{},coroutine 立马暂停(suspending, lazily)
  3. return_void(): 定制coroutine到达结尾或者碰到co_return 语句时的行为。当promise_type声明并定义了该成员函数,那么coroutine必须不能返回任何值。
  4. unhandled_exception(): 定制coroutine处理异常的方式。可以重新抛出异常也可以直接调用std::terminate()
  5. final_suspend(): 定制coroutine是否被最终suspend。这个函数需要保证不能抛出异常,且应该总是返回std::suspend_always{}

std::coroutine_handle

coroutine_handle表示一个正在执行或者暂停(suspend)的coroutine。在C++20中,coroutine_handle由标准模版 std::coroutine_handle<> 表示。 std::coroutine_handle的模版参数为promise_type的类型。

当你调用一个coroutine时,编译器会为你创建一个coroutine frame。该coroutine frame中会存放coroutine相应的状态。 为了恢复coroutine的执行或者销毁coroutine frame,你需要一个coroutine handle,其用来定位相应的coroutine frame。

coroutine接口

coroutine接口,也即是coroutine返回给coroutine调用者的实例,用来与coroutine调用者进行通信的媒介。

若要通过coroutine接口来定制coroutine的行为,需要在coroutine接口中实现promise_type以及定义std::coroutine_handle成员。

接口实践

若想将coroutine中的一些信息返回给coroutine调用者,可以初步使用co_yield和co_return这两个关键字。

#include <iostream>
#include <coroutine>
#include <string>

class CoroTask {
public:
  struct promise_type;
  using CoroHd = std::coroutine_handle<promise_type>;

  CoroTask(auto hd) : hd_{hd} {}
  ~CoroTask() {
    if (hd_) { hd_.destroy(); }
  }

  CoroTask(const CoroTask&) = delete;
  CoroTask& operator=(const CoroTask&) = delete;

  int getVlaue() const { return hd_.promise().CoroValue; }

  bool resume() {
    if (hd_ && !hd_.done()) {
        hd_.resume();
        return true;
    }
    return false;
  }

public:
  struct promise_type {
    int CoroValue{};
    /* data */
    auto get_return_object() {
        return CoroTask{CoroHd::from_promise(*this)};
    }
    auto initial_suspend() { return std::suspend_always{};}
    void unhandled_exception() { std::terminate();}
    auto yield_value(int val) {
      CoroValue = val;
      return std::suspend_always{};
    }
    void return_void() {}
    auto final_suspend() noexcept { return std::suspend_always{}; } 
  };

private:
  CoroHd hd_;
};

#include "coro_task.h"

CoroTask coro(int max) {
    std::cout << "coro start, max: " << max << "\n";

    for (int i = 0; i <= max; ++i) {
        std::cout << "coro index: " << i << "\n";
        co_yield i;
    }
    std::cout << "coro end\n";
}

int main() {
    auto task = coro(4);
    while (task.resume()) {
        std::cout << "main get coroutine value: " << task.getVlaue() << "\n";
    }

    return 0;
}

若要使用co_yield将coroutine中的某些信息传递给coroutine调用者需要coder做如下工作:

  • 在promise type类中声明并定义所需要的信息结构
  • 在promise type类中声明并定义yield_value接口,并将该接口的参数设置为相应的信息结构的类型
  • 在yield_value的实现中完成信息的写入
  • 在coroutine接口(coroutine返回值)中提供一个获取信息结构的接口,在该接口的实现中通过std::coroutine_handle的promise()接口获取coroutine相关的promise type并取回相应的值。

利用co_return返回信息给coroutine调用者

co_return将coroutine中产生的数据返回给coroutine调用者,并在coroutine调用者中输出相应的内容。

template <typename T>
class CoGen {
public:
  class promise_type {
  public:
    CoGen get_return_object() {
      return CoGen{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    void return_value(const std::vector<T>& vec) {
      vec_ = vec;
    }
    auto initial_suspend() { return std::suspend_always{};}
    void unhandled_exception() { std::terminate();}
    auto final_suspend() noexcept { return std::suspend_always{}; } 
    const std::vector<T>& getResult() const { return vec_; }

  private:
    std::vector<T> vec_{};
  };
  CoGen(auto hd) : hd_{hd} {}
  ~CoGen() {
    if (hd_) {
       hd_.destroy();
    }
  }

  std::vector<T> getResult() const { return hd_.promise().getResult(); }
  bool resume() {
    if (!hd_ || hd_.done()) return false;
    hd_.resume();
    return true;
  }

private:
  std::coroutine_handle<promise_type> hd_{};
};

CoGen<int> coroGen(int max) {
    std::vector<int> vec;
    vec.resize(abs(max));
    std::iota(vec.begin(), vec.end(), 0);
    co_return vec;
}

int main() {
    auto task = coroGen(7);
    while (task.resume());
    std::cout << "coroutine end\n";
    for (const auto& val : task.getResult()) {
        std::cout << " " << val;
    }
    std::cout << "\n";
    return 0;
}