0%

Callables

Callable 有很多:函数、函数指针、成员函数、lambda 表达式(函数对象特例)、函数对象(Functor)等。

lambda 表达式

lambda 是函数对象。当捕获列表没有参数的时候,lambda 还有一个转换成一般函数指针的隐式转换操作符。在无捕获列表的 lambda 前加上 + 符号,能使得 lambda 变成函数指针也就是这个道理。

std::invoke

支持 Callable 并不简单,因为需要考虑到成员函数的调用需要通过 this 指针来完成。C++17 提供了 std::invoke 来将这些操作统一起来,只需要把 this 指针当作第一个参数传入。

Surrogate Function - 从类型隐式转换得来

Dummy/Surrogate function(哑函数/替代函数)指的是某一个函数对象还具有一个到普通函数引用或者普通函数指针的隐式转换,这个转换好的函数就是替代函数。替代函数用到了一次隐式转换,所以在参数匹配均不优于函数对象的其他()操作符重载时不会被选中。但若替代函数是更好的替代,将出现函数使用 ambiguous 的情况。

class IndirectFunctor {
   public:
    void operator()(double, double) const {}
    operator auto () const {
        return +[](double, int) {};
    };
};

void activate(IndirectFunctor const& funcObj) {
    funcObj(3, 5); // ERROR: ambiguous
}

1 参数化声明

模板分类

模板有四类:类模板、函数模板、变量模板、别名模板。其中别名模板是:

变量模板其实可以不需要 inline(但是最好加上)

C++17 中静态和全局变量都能够用 inline 来修饰。这也意味着类中的静态变量不必在类中声明、类外初始化(非头文件),而是可以在类中直接写 inline。之前笔记里面提到的问题里,inline 加不加都没有关系,因为变量模板本身可以在多个翻译单元中出现。P179

嵌套模板的非原地定义

想要给类模板中的模板在类外提供定义是比较复杂的,需要嵌套模板参数描述:

名字查找:普通查找和 ADL

普通查找和 ADL 是同时进行的,不存在优先级差异。这些被找到的函数被放在一起参与重载决议。如果没有因为一般性的重载决议规则决出优劣,则会引发 ambiguous 指代错误。

Ordinary lookup: 内部名称遮盖外部名称的规则

ADL: 考虑参数类型所在名字空间的函数和操作符重载

相关类型有很多规则,比如指针会引入其指向类型的名字空间、类会引入外围类(如果是一个内部类)和基类的名字空间、函数会引入其参数和返回值的名字空间等(P219)。特例是 hidden friends,可以直接定义在类中。

ADL 失效的情况:① 调用前给函数名加上括号,这样就必须先找到函数。② 函数模板在 C++20 之前除非在外部声明函数模板(参数列表可以不同),否则也无法被找到。因为在还没有看参数的情况下,函数模板没有被引入;还没有看参数列表的情况下不知道这个是函数模板;不知道这个是函数模板的情况下会把参数列表的头尾理解成大于和小于操作符。这是一种鸡生蛋蛋生鸡的问题。③ ADL 会忽略 using 声明,只考虑真正存在于名字空间的函数。

ADL 只会通过参数(也就是显式调用)引入额外的函数和操作符,其他情况下这些名称是不会被自动引入的,比如取一个和参数相关的函数指针。

2023 年 5 月 17 日

推导上下文:Deduced Context

P272 书上

  • 依赖于模板类型的子类型不是推导上下文。比如 typename X<N>::I 不是。而 X<N>::* 这样的指向成员的指针没有用到子类型,所以是推导上下文。
  • 非类型模板参数的非平凡表达式不是推导上下文。比如模板 S 和参数 IS<I+1> 无法提供推导信息。

如何推导出模板参数

  1. 从参数列表中推导(推导能力是有限制的,比如不能从 typename T::iterator 参数中推出 T 的类型)
  2. 函数模板被取地址时,可以从要求的返回值类型推导:
template<typename T>
void f(T, T);

void (*pf)(char, char) = &f; // f的参数T由函数指针的类型确定

这一章讲的是对未来的展望。

浮点数和字符串作为模板参数

浮点数在 gcc C++20 是可以作为非类型模板参数(NTTP)的;clang 不支持。目前 C++ compiler support - cppreference.com 上也看不到相关的支持项目。

字符串字面量在 C++20 只能通过隐式转换成简单的对象实现模板参数。15/特例:变参列表/参数包 一节有描述。字符数组则是早就可以作为模板参数,尽管作为引用其 linkage 在 C++11 和 C++14 有额外的要求。

当前 class type 和浮点数没有作为非模板参数得到全面的支持,原因很可能是相等性比较链接阶段很难进行(浮点数的标准可能不同,或者因为精度缺失会误判;类可能会重载比较操作符)。

减少类模板 pack expansion 的限制

目前不能在类模板参数中使用多个 pack expansion,而且使用时也必须把它放在最末尾的位置;不仅模板参数声明是这样,填充类模板参数也是这样。

C++ 的多态有两类:bounded dynamic polymorphism 和 unbounded static polymorphism。前者 bounded 指的是虚函数有基类的接口,其行为有一定约束;后者 unbounded 指的是模板采用类型替换的方式生成代码。为了给静态多态提供约束,可以使用 enable_if 或 concept。

Trait 分类

不一定大家都这么叫,但是为了方便,我规定下面的术语:

  • Type Trait:包含一个 type alias。
  • Value Trait:包含一个 value 静态成员,可以是任何基本类型,含义和具体 Trait 相关。
  • Predicate Trait:包含一个 value 静态成员,类型为 bool,含义为条件是否被满足。

书中有分为 property trait 和 policy trait 两大类。前者包含我所称的 type trait 和 value trait,含义是类型本身的固有信息;后者表示根据一个特定类型选择某种映射,其含义和要实现的行为有关,文中的例子是对 size 小的参数选择原类型,对 size 大的参数选择引用类型。

Traits v.s. Policies

例子是累加算法的模板实现。

Traits 一般表示和类型有关的静态的信息;Policies 表示行为。累加例子中 Traits 包含加法结果的类型,而 Policies 则包含了加法的执行过程。

原理是用 SFINAE 机制安全地匹配几个函数或函数模板,然后再通过函数匹配信息将结果(true or false)嵌入 Trait 类中。Trait 约束条件被包含在函数模板的声明中。

实现 traits 最核心的要点是模拟被替换类型的行为,然后让行为不满足的那些替换被 SFINAE 筛掉。

函数返回基本类型 → 静态常量定义

类模板实例化时会调用 test 函数,如果条件命中则优先匹配 char test(void *),否则匹配另外一个。通过判断匹配到的函数的返回值类型可以确定条件是否被命中。这里的条件是类型 T 可以默认构造。

⚠ 如果把第一个 test 的参数 U 去掉,默认参数改成 typename = decltype(T()),就无法编译。因为 T 会在实例化时被替代过去,导致函数模板本身定义有错误。这个过程没有发生在函数模板匹配阶段,不会受到 SFINAE 的保护。

TLDR:几种 SFINAE 的模式

  1. 对于函数参数列表非空的函数模板,可以用非 varargs 函数实现功能,varargs 作为 fallback。
  2. 类模板可以用偏特化实现功能重载,用带模板默认参数的原始模板作为 fallback。
  3. 模板用 tag dispatching 不会有重复定义的问题。
  4. std::enable_if 用在函数模板的模板默认参数上表达更简洁,但用在返回值上可避免重复定义。对比起来,类模板虽然能支持偏特化,但是也没有用返回值 SFINAE 避免重复的好处。(不过 C++20 有了 requires 就无所谓了。)
  5. concept、constexpr if 等。

1 和 2 其实分别适应了函数模板和类模板的特点:函数模板不能偏特化,类模板不是函数因而不能使用 varargs。

几个难点:

  1. 找不到能匹配的函数:需要设计 fallback。
  2. 找到了多个能匹配且优先级不能区分的函数(ambiguous):需要设计互斥条件。
  3. 重复定义同一模板(只有模板默认参数不同):std::enable_if 使用不当。可以再增加一个默认参数缓解问题,或者把 std::enable_if 放在返回值位置上。

函数模板的重载

简单提供参数不同的重载