回调

  • 回调的含义是:对一个库,用户希望库能够调用用户自定义的某些函数,这种调用称为回调。C++中用于回调的类型统称为函数对象类型,它们能直接用作函数实参 ```cpp

    include

    include

template void foreach(Iter current, Iter end, Callable op) { while (current != end) { op(*current); ++current; } }

void func(int i) { std::cout << i << ‘\n’; }

struct FuncObj { void operator()(int i) const { std::cout << i << ‘\n’; } };

int main() { std::vector primes = { 2, 3, 5, 7, 11, 13, 17, 19 }; foreach(primes.begin(), primes.end(), func); foreach(primes.begin(), primes.end(), &func); foreach(primes.begin(), primes.end(), FuncObj()); foreach(primes.begin(), primes.end(), [] (int i) { std::cout << i << ‘\n’; }); }

  1. <a name="41eaf6d3"></a>
  2. ## 处理成员函数和附加实参
  3. - C++17提供了[std::invoke](https://zh.cppreference.com/w/cpp/utility/functional/invoke)
  4. ```cpp
  5. // basics/foreachinvoke.hpp
  6. #include <utility>
  7. #include <functional>
  8. template<typename Iter, typename Callable, typename... Args>
  9. void foreach (Iter current, Iter end, Callable op, const Args&... args)
  10. {
  11. while (current != end)
  12. {
  13. std::invoke(op, args..., *current);
  14. ++current;
  15. }
  16. }
  • 这里除了函数对象,还能接收任意数量的附加参数。如果函数对象是一个类成员指针,使用第一个附加实参作为this对象,其余作为实参传递给函数对象,否则所有附加参数都只传递给函数对象 ```cpp

    include

    include

    include

    include “foreachinvoke.hpp”

class A { public: void f(int i) const { std::cout << i << ‘\n’; } };

int main() { std::vector primes = { 2, 3, 5, 7, 11, 13, 17, 19 }; foreach(primes.begin(), primes.end(), // 范围内的元素是lambda的第二个参数 { std::cout << prefix << i << ‘\n’; }, “value: “); // lambda的第一个参数

A obj; foreach(primes.begin(), primes.end(), &A::f, obj); }

  1. <a name="671e31b2"></a>
  2. ## 包裹函数调用
  3. - [std::invoke](https://zh.cppreference.com/w/cpp/utility/functional/invoke)的一个常见应用是包裹单个函数调用,为了支持返回引用(如std::ostream&),这里使用decltype(auto)替代auto
  4. ```cpp
  5. template<typename Callable, typename... Args>
  6. decltype(auto) call(Callable&& op, Args&&... args)
  7. {
  8. return std::invoke(std::forward<Callable>(op),
  9. std::forward<Args>(args)...);
  10. }
  • 如果想临时存储std::invoke返回的值,也必须用decltype(auto)声明临时变量

    1. template<typename Callable, typename... Args>
    2. decltype(auto) call(Callable&& op, Args&&... args)
    3. {
    4. decltype(auto) ret{std::invoke(std::forward<Callable>(op),
    5. std::forward<Args>(args)...)};
    6. return ret;
    7. }
  • 注意,把ret声明为auto&&是不正确的,auto&&作为一个引用,生命周期不会超出return语句

  • 但使用decltype(auto)也有一个问题,如果函数对象返回void类型,把ret初始化为decltype(auto)是不允许的,因为void是一个不完整的类型
  • 一个解决方法是在那条语句之前声明一个对象,该对象的析构函数执行希望实现的可观察的行为 ```cpp struct cleanup { ~cleanup() { … // code to perform on return } } dummy;

return std::invoke(std::forward(op), std::forward(args)…);

  1. - 另一个方法是使用if constexpr实现不同的分支
  2. ```cpp
  3. template<typename Callable, typename... Args>
  4. decltype(auto) call(Callable&& op, Args&&... args)
  5. {
  6. if constexpr (std::is_same_v<std::invoke_result_t<Callable, Args...>, void>)
  7. {
  8. std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...);
  9. return;
  10. }
  11. else
  12. {
  13. decltype(auto) ret{std::invoke(std::forward<Callable>(op),
  14. std::forward<Args>(args)...)};
  15. return ret;
  16. }
  17. }

实现泛型库的其他工具

type traits

template class C { static_assert(!std::is_same_v,void>, “invalid instantiation of class C for void type”); public: template void f(V&& v) { if constexpr(std::is_reference_v) { … // 如果T是引用类型 } if constexpr(std::is_convertible_v,T>) { … // 如果V能转换为T } if constexpr(std::has_virtual_destructor_v) { … // 如果V有析构函数 } } };

  1. - 注意[type traits](https://zh.cppreference.com/w/cpp/header/type_traits)可能与预期表现不符
  2. ```cpp
  3. std::remove_const_t<const int&> // 生成const int&
  • 这里是引用不是const,所以调用没有效果。移除引用和const的顺序不同会导致不同的结果

    1. std::remove_const_t<std::remove_reference_t<const int&>> // int
    2. std::remove_reference_t<std::remove_const_t<const int&>> // const int
  • 也可以直接调用std::decay

    1. std::decay_t<const int&> // int
  • type traits也会有不满足要求导致未定义行为的情况

    1. make_unsigned_t<int> // unsigned int
    2. make_unsigned_t<const int&> // undefined behavior (hopefully error)
  • 有时结果可能出乎意料

    1. add_rvalue_reference_t<int> // int&&
    2. add_rvalue_reference_t<const int> // const int&&
    3. add_rvalue_reference_t<const int&> // const int&(由于引用折叠,左值引用仍为左值引用)
    4. is_copy_assignable_v<int> // true(一把可以把int赋给int)
    5. is_assignable_v<int, int> // false(不能调用42 = 42)
  • is_copy_assignable只检查能否把int赋给另一个(检查左值操作),is_assignable则考虑到值类型(这里检查能否把右值赋给右值),因此第一个表达式等价于

    1. is_assignable_v<int&, int&> // true
  • 同理

    1. is_swappable_v<int> // true(假设是左值)
    2. is_swappable_v<int&, int&> // true(等价于上一行)
    3. is_swappable_with_v<int, int> // false(考虑值类型)

std::addressof

  • std::addressof函数模板产生一个函数或对象的地址,即使对象类型重载了&,因此需要一个依赖于模板参数的地址时推荐使用std::addressof

    1. template<typename T>
    2. void f(T&& x)
    3. {
    4. auto p = &x; // 如果重载了operator&就可能失败
    5. auto q = std::addressof(x); // 即使重载了operator&也能工作
    6. ...
    7. }

    std::declval

  • std::declval可以获取对象类型,但无需构造对象 ```cpp struct Default { int foo() const { return 1; } };

struct NonDefault { NonDefault(const NonDefault&) {} int foo() const { return 1; } };

int main() { decltype(Default().foo()) n1 = 1; // n1类型为int decltype(NonDefault().foo()) n2 = n1; // 错误:无默认构造函数 decltype(std::declval().foo()) n2 = n1; // n2类型为int }

  1. - 比如下面的声明从T1T2推断默认返回类型RT,为了避免调用T1T2的构造函数,使用[std::declval](https://zh.cppreference.com/w/cpp/utility/declval)获取对应对象但不创建。使用[std::declval](https://zh.cppreference.com/w/cpp/utility/declval)必须确保默认返回类型不能为引用,它本身产生右值引用
  2. ```cpp
  3. template<typename T1, typename T2,
  4. typename RT = std::decay_t<decltype(true ?
  5. std::declval<T1>() : std::declval<T2>())>>
  6. RT max(T1 a, T2 b)
  7. {
  8. return b < a ? a : b;
  9. }

完美转发临时对象

  1. template<typename T>
  2. void f(T&& x)
  3. {
  4. g(std::forward<T>(x)); // 完美转发实参x给g()
  5. }
  • 然而有时不是直接地完美转发

    1. template<typename T>
    2. void f(T x)
    3. {
    4. g(doSomething(x));
    5. }
  • 如果想在转发前修改要转发的值,可以用auto&&存储结果,修改后再转发

    1. template<typename T>
    2. void f(T x)
    3. {
    4. auto&& res = doSomething(x);
    5. doSomethingElse(res);
    6. set(std::forward<decltype(res)>(res));
    7. }

模板参数为引用的情况

  • 尽管不常见,模板类型参数可以变成引用类型 ```cpp

    include

template void tmplParamIsReference(T) { std::cout << std::is_reference_v << ‘\n’; }

int main() { std::cout << std::boolalpha; // 之后打印true将为true而不是1 int i; int& r = i; tmplParamIsReference(i); // false tmplParamIsReference(r); // false tmplParamIsReference(i); // true tmplParamIsReference(r); // true }

  1. - 而显式指定则可以强制T为引用,一些模板设计时没有考虑这个问题,就可能引发错误和未定义行为
  2. ```cpp
  3. template<typename T, T Z = T{}>
  4. class RefMem {
  5. public:
  6. RefMem() : zero{Z} {}
  7. private:
  8. T zero;
  9. };
  10. int null = 0;
  11. int main()
  12. {
  13. RefMem<int> rm1, rm2;
  14. rm1 = rm2; // OK
  15. RefMem<int&> rm3; // ERROR: invalid default value for N
  16. RefMem<int&, 0> rm4; // ERROR: invalid default value for N
  17. extern int null;
  18. RefMem<int&,null> rm5, rm6;
  19. rm5 = rm6; // ERROR: operator= is deleted due to reference member
  20. }
  • 对非类型模板参数使用引用类型也很危险 ```cpp

    include

    include

template // 注意:SZ是引用 class Arr { public: Arr() : elems(SZ) {} void print() const { for (int i = 0; i < SZ; ++i) { std::cout << elems[i] << ‘ ‘;
} } private: std::vector elems; };

int size = 10;

int main() { Arr y; // compile-time ERROR deep in the code of class std::vector<> Arr x; // initializes internal vector with 10 elements x.print(); // OK size += 100; // OOPS: modifies SZ in Arr<> x.print(); // run-time ERROR: invalid memory access: loops over 120 elements }

  1. - 上面这个例子有些牵强,但在更复杂的情况下确实可能发生,在C++17中非类型参数可以被推断,比如
  2. ```cpp
  3. template<typename T, decltype(auto) SZ>
  4. class Arr;
  • 使用decltype(auto)很容易产生引用类型。因此通常在这里会默认使用auto,标准库因此也有一些令人惊讶的规约限制,比如即使模板参数初始化为引用,为了仍然有赋值运算符,std::pairstd::tuple实现了赋值运算符,而不是使用默认行为

    1. namespace std {
    2. template<typename T1, typename T2>
    3. struct pair {
    4. T1 first;
    5. T2 second;
    6. ...
    7. // default copy/move constructors are OK even with references:
    8. pair(pair const&) = default;
    9. pair(pair&&) = default;
    10. ...
    11. // but assignment operator have to be defined to be available with references:
    12. pair& operator=(pair const& p);
    13. pair& operator=(pair&& p) noexcept(...);
    14. ...
    15. };
    16. }
  • 又比如为了避免可能造成的副作用的复杂性,C++17的类模板std::optionalstd::variant对引用是非法的

  • 只需要使用简单的static断言就可以禁用引用
    1. template<typename T>
    2. class optional {
    3. static_assert(!std::is_reference<T>::value,
    4. "Invalid instantiation of optional<T> for references");
    5. ...
    6. };

延迟计算

  • 实现模板时,有时代码是否能处理不完整类型也会引发问题

    1. template<typename T>
    2. class Cont {
    3. private:
    4. T* elems;
    5. public:
    6. ...
    7. };
  • 目前这个类能用于不完整类型

    1. struct Node {
    2. std::string value;
    3. Cont<Node> next; // 只有Cont能接受不完整类型时可行
    4. };
  • 然而如果使用一些type traits,可能就会失去处理不完整类型的能力

    1. template<typename T>
    2. class Cont {
    3. public:
    4. std::conditional_t<std::is_move_constructible_v<T>, T&&, T&> foo();
    5. private:
    6. T* elems;
    7. };
  • 这里用std::conditional决定返回类型为T&&还是T&,这依赖于T是否支持移动语义。问题在于is_move_constructible要求实参是完整类型(且不是void或一个数组的未知绑定),于是带有这个声明的struct node声明也会失败

  • 可以用一个成员模板替代成员函数解决此问题,这样is_move_constructible的计算会延迟到成员模板的实例化点
    1. template<typename T>
    2. class Cont {
    3. public:
    4. template<typename U = T>
    5. std::conditional_t<std::is_move_constructible_v<U>, T&&, T&> foo();
    6. private:
    7. T* elems;
    8. };

编写泛型库的考虑事项

  • 使用转发引用完美转发模板中的值。如果值需要改动,使用auto&&存储值
  • 当参数被声明为转发引用,传递左值时,模板参数会被推断为引用类型
  • 需要一个依赖于模板参数的地址时,使用std::addressof以防参数被绑定到一个重载了operator&的类型
  • 确保成员函数模板不是比默认的拷贝/移动构造函数或赋值运算符更好的匹配
  • 当模板参数可能是字符串字面值并且不是按值传递时,考虑使用std::decay
  • 如果需要一个输入输出参数,它返回一个新对象或允许修改实参,传non-const引用(也可以按指针传递),但注意要考虑意外接收const对象的情况
  • 考虑模板参数为引用的情况,尤其是想确保返回类型不能变成一个引用时
  • 考虑对不完整类型的支持,比如递归的数据结构
  • 对所有数组类型重载,而不只是T[SZ]