0%

我的理解是:C++ 的协程是无栈的,这意味着协程只是计算任务,仅在运行时需要栈,在 suspend 之后就会保存状态并脱离栈。要想要跨线程转移计算任务(比如实现工作窃取池),只需要将离栈协程在另外一个线程上 resume 即可。

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>

auto switch_to_new_thread(std::jthread& out)
{
    struct awaitable
    {
        std::jthread* p_out;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h)
        {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("Output jthread parameter not empty");
            out = std::jthread([h] { h.resume(); });
            // Potential undefined behavior: accessing potentially destroyed *this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
        void await_resume() {}
    };
    return awaitable{&out};
}

struct task
{
    struct promise_type
    {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

task resuming_on_new_thread(std::jthread& out)
{
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    // awaiter destroyed here
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}

int main()
{
    std::jthread out;
    resuming_on_new_thread(out);
}

可能结果:

Coroutine started on thread: 139972277602112
New thread ID: 139972267284224
Coroutine resumed on thread: 139972267284224

2023 年 5 月 7 日

两个 type traits

std::decay_t 可以去引用、去限定符、函数/数组变指针;std::common_type_t 用三元运算符获得更“宽泛”类型。

函数模板默认参数

模板默认参数可以放在最前面,不像普通函数只能把带有默认值的参数放在最后。而且函数模板可以明确指定开头几个参数,让后面的参数由推导规则生成。

函数重载参数匹配优先级

优先级排序

  1. perfect match
  2. decay,或者修改指针的内外层 const 属性(volatile 应该也算吧?) 比如 char *char const *
  3. promotion,比 int 小的转到 int 或更大的整数,或者 float 到 double。 举例:bool 到 char 不是 promotion,而是 standard conversion
  4. standard conversion,包括 int to float子类(值/引用/指针)向基类(值/引用/指针)转换
  5. 用户定义的转换,包括标准库的类
  6. 与可变长参数列表匹配,也就是 (...) 例外:f(...) f(void) 参数类型是同级别的,因为没有提供参数,编译器不知道哪一个匹配更合适。

以上参数匹配不涉及到值类型、左引用、右引用的区别。只要引用属性能够匹配,就不区分优先级。比如同时匹配到 f(int)f(int &) 编译器就会抱怨函数调用 ambiguous。

2023 年 5 月 7 日

定义位置

类模板只能在全局或者类声明中定义,不能在块或者函数中定义。

类模板外函数定义的写法

类模板中定义函数,T 类型参数可以省略;在类模板中声明,但在外提供函数定义,就需要在定义处给出完整的类型参数:

template<typename T> void Stack<T /* 不能省略 */>::push (T const& elem) { /* impl */ }

一个例外是析构函数和构造函数的名称处,因为这里不表类型,而是表示函数名。

模板参数也可以不是类型,而是值。

非类型参数推导只对直接调用有效

模板函数的类型推导只对立即调用有效,而对算法模板这类需要提前知道类型信息的场合无效。比如:

std::transform (source.begin(), source.end(), // start and end of source
                dest.begin(),     // start of destination
                addValue<5,int>); // operation

上面代码中尽管 addValue 的参数可能可以推导出来,但由于 std::transform 要求提前知道函数的类型,去掉参数之后代码无法编译。

非类型参数的类型限制

非类型参数不能是浮点数(未来可能可以)。只能是整数、nullptr、指针或左值引用。

可变长表达式可以对参数包逐个处理。与折叠表达式(相当于函数式编程中的 reduce,但如果用上逗号表达式就能表达 foreach)不同,可变长表达式(相当于函数式编程中的 map)不会改变参数数量,允许的形式也比折叠表达式宽松一点。

参数包(Parameter Pack)和可变长模板

模板函数可以指定参数包,用 sizeof…() 运算符可以获得参数包的数量。至少有一个参数包的模板称为可变长模板(variadic template)

0 Pack Expansion:参数传递

参数传递。

Note

2024 年 2 月 20 日:除了作为函数参数之外,还能用作初始化列表(花括号)的参数。

1 递归:逐个处理

#include <iostream>
void print ()
{
}

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << firstArg << '\n'; // print first argument
    print(args...);                // call print() for remaining arguments
}

1 使用关键字引导编译器了解成员类型

template<typename T>
class MyClass {
public:
    void foo() {
        typename T::SubType* ptr;
    }
}

上面的的代码中加了 typename 关键字用来表示 SubType 这个成员是一个类型。否则由于缺乏类型信息,编译器会认为该成员是某个数据域,从而构成了乘法表达式。

template<unsigned long N>
void printBitset (std::bitset<N> const& bs) {
    std::cout << bs.template to_string<char, std::char_traits<char>, std::allocator<char>>();
}

上面的 template 关键字在 to_string 前作解释,表示 to_string 是一个函数模板,否则后面的尖括号会被认为是小于运算符,而不是函数模板参数的开始标志。

2 正确初始化模板类的成员

在模板类中为了保证成员被正确初始化,应该使用 {} 初始化的语法,因为无论 T 是什么类型,这种初始化方式都是可行的。此外 {} 也能用在数据成员定义处,或者构造函数初始化列表中:

Perfect forwarding:

template<typename T>
void f (T&& val) {
    g(std::forward<T>(val)); // perfect forward val to g()
}

注意,完美转发只对模板参数有效,对依赖于模板参数的类型也是无效的,比如:typename T::iterator&& 是无效的,仍表示严格的右值引用。

完美转发也不能加 const 属性。添加 const 属性后表示常右值引用(而且这种形式很少被使用)。

尝试用完美转发简化代码

设想一个 Person 类,里面只有一个 std::string 类型的 name 数据。已经实现了复制构造函数和移动构造函数。从数据的构造函数有 std::string &&std::string const & 两个重载版本。使用完美转发可以同时写出匹配这两种参数的函数模板,减少代码量。

传值:T

有 decay。

传左值引用:T &

注意遇到数组时可能需要 decay。

注意 T 类型可能是常量,导致 T & 其实是常引用。这样的参数不能被修改,因而不能作为传出参数。可以使用 concept 限定参数非常量:

传转发引用:T &&

这是 T 唯一可能匹配到引用类型的情况。如果 T 本身就是引用类型,就不能用它创建值类型的同类数据。解决方法有:① 用 type traits 去除引用属性 ② 使用 auto,因为 auto 默认不会匹配到引用。

模板元编程

略。

constexpr 函数

在 C++11 中只能使用一条语句,而在 C++14 之后可以使用的语句变得丰富。constexpr 函数允许编译期优化,但不阻止运行期使用。作为对比,C++20 consteval 只能在编译期使用。

应用:e.g. 常量表达式函数可以计算出一些基本信息,比如判断一个整数是否为素数,以此做为另一个模板类偏特化的依据。

SFINAE

substitution failure is not an error

有一些地方需要注意:寻找函数 candidates 时只会考虑函数模板的函数签名,不会考虑函数体。用 C++14 auto 省略掉返回值类型之后,也不会考虑返回值类型。如果匹配完成之后函数体不能编译,编译器就会报错。

包含模型 - The Inclusion Model

通常模板需要被包含在头文件中工作。

注意:函数模板完全特化之后也需要 inline 才能够在头文件中使用,否则会出现不同的翻译单元有重复定义的情况。

改进一:precompiled headers

可以把 pch 理解为编译器中间工作状态的一个 dump。编译器可以在读取到某些源代码之后保存其状态,包括符号表等。如果两个文件中有公共的前缀代码(比如一个标准库的头文件就包含了数千行公共代码),编译器就可以读取预先保存的状态,从而跳过对于这些代码的处理。

很遗憾的是前缀的判定是必须完全相同,即便改变头文件的包含顺序,其中与宏相关的定义也可能结果不同。由于宏无法从模块中导出,模块将可能解决这一痛点。

改进二:explicit template instantiation

只在头文件中留模板函数的声明,而不是定义。然后在其他的源文件中提供特化的定义。