CTTCG 11 Generic Libraries

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
}

上面的代码若将 operator auto() const 中的 lambda 表达式的参数类型改成 (double, double),该函数就会因为需要一次额外的隐式转换而落选,代码能够正常编译。

Type Traits

std::decay_t<int const&>int

remove_const 不能去掉引用的 const 属性,但是 decay 可以。

std::add_rvalue_reference_t<int const&>int const &

左值引用的右值引用还是左值引用。

std::is_assignable_v<int,int>false

std::is_assignable_v<int&,int>true

注意值类型是不能赋值的,这个检查比类型间是否可以转换更加严格。

std::is_swappable_v<int> yields true (assuming lvalues),默认左值

std::is_swappable_with_v<int&,int&> yields true (equivalent to the previous check)

std::is_swappable_with_v<int,int> yields false (taking value category into account)

带有 with 的是接受两个参数的。

std::conditional 能够接受一个条件和两个类型,按照三元表达式的处理逻辑返回一个类型:

std::addressof()

用来获取真实地址。可以规避 & 运算符重载的影响。

std::declval<T>()

这个函数模板用于创建一个 **T&&** 类型的值。该值只能用于提供类型信息,不能被真正使用。应用场景有 type traits 或者 decltype 里面。注意由于返回的是转发引用,所以对非左值引用的参数类型返回的是右值引用,其结果可能需要我们做去引用或者 decay 处理。

完美转发

一般的转发是这样的:

下面讨论一些棘手的情况。

传参场景 1:多次调用同一函数时参数可能已被移动

如果正在遍历一个范围,需要反复多次调用同一函数时,就不能使用完美转发了。因为调用一次后,函数对象和其他参数都可能会被这次调用移动。这个时候只能传值或者常引用。

传参场景 2:链式调用

可能需要对参数的调用结果做出一定处理之后继续使用。这个时候可以用 auto && 来承接返回值。

用 auto && 相比 decltype(auto) 可以省去之后调用 std::move 的麻烦,因为我们明确不需要链式调用的中间值。

💡 注意与 std::declval 不同,std::move 的结果是一定会变成右值的。而 std::declval 是尝试加右值引用,可能会折叠成左值引用。

虽然中间值用 auto &&,返回值还是用 decltype(auto)(见下面)。

正确使用 auto 推导返回值

函数返回值类型应该视情况被声明为 decltype(auto) 。与其他几种形式的区别是:auto 不能保留引用, auto && 在没有引用的时候会多添加引用属性(在函数体内使用当然是没问题的,因为在作用域内有生命周期延长,所以不会悬挂,但是作为返回值就有问题)。

其次,函数中真正用到的最后一个被直接返回的值也要声明为 decltype(auto)

💡 decltype(auto) 在参数上面的使用可能会在后面章节讨论。一般来说还是要谨慎在模板参数中使用带引用的 autodecltype(auto) 可能会被推导为引用)。

void 类型不被能 decltype(auto) 或者 auto && 接受

if constexpr 表达式判断返回值类型:

if constexpr(std::is_same_v<
    std::invoke_result_t<Callable, Args...>, void>)

然后分情况处理。在返回值类型为 void 的时候就省略掉对返回值的利用。

对字面量的推导

完美转发前首先需要对参数类型推导,这相当于禁用了类型之间的隐式转换,见 完美转发

模板参数是引用

比较危险。标准库做出了专门处理,或者拒绝参数为引用的情形。

延迟评估

有些类模板不需要参数是完整类型,比如智能指针。

如果类模板中出现了一些函数对参数 T 的完整性有要求,编译就可能向错误的方向发生。在智能指针的场景下,一个链表的定义中很可能包含了不完整的类型(Node),这就符合上面的情况。

这张图中的可移动构造的 type trait 就要求类型 T 是完整类型,否则将返回 false。处理方案是把这个函数声明变成函数模板声明:

函数模板只会在用到的时候被实例化,这样就延后了对于 std::conditional 条件的判断,到时 DT 应该已经是一个完整类型。