• CPP-Templates-2nd--第十一章 泛型库


    目录

    11.1 可调用对象(Callables) 

    11.1.1 函数对象的支持

    11.1.2 处理成员函数以及额外的参数

    11.1.3 函数调用的包装

    11.2 其他一些实现泛型库的工具

    11.2.1 类型萃取

    11.2.2 std::addressoff()

    11.2.3 std::declval()

    11.3 完美转发临时变量

    11.4 作为模板参数的引用

    11.5 推迟计算(Defer Evaluation)

    11.6 在写泛型库时需要考虑的事情

    11.7 总结


    参考:GitHub - Walton1128/CPP-Templates-2nd--: 《C++ Templates 第二版》中文翻译,和原书排版一致,第一部分(1至11章)以及第18,19,20,21、22、23、24、25章已完成,其余内容逐步更新中。 个人爱好,发现错误请指正

    11.1 可调用对象(Callables) 

    一些库包含这样一种接口,客户端代码可以向该类接口传递一个实体,并要求该实体必须被 调用。相关的例子有:必须在另一个线程中被执行的操作,一个指定该如何处理 hash 值并 将其存在 hash 表中的函数(hash 函数),一个指定集合中元素排序方式的对象,以及一个 提供了某些默认参数值的泛型包装器。标准库也不例外:它定义了很多可以接受可调用对象 作为参数的组件。

    这里会用到一个叫做回调(callback)的名词。传统上这一名词被作为函数调用实参使用, 我们将保持这一传统。比如一个排序函数可能会接受一个回调参数并将其用作排序标准,该 回调参数将决定排序顺序。

    在 C++中,由于一些类型既可以被作为函数调用参数使用,也可以按照 f(...)的形式调用,因 此可以被用作回调参数:

     函数指针类型

     重载了 operator()的 class 类型(有时被称为仿函数(functors)),这其中包含 lambda 函数

     包含一个可以产生一个函数指针或者函数引用的转换函数的 class 类型

    这些类型被统称为函数对象类型(function object types),其对应的值被称为函数对象 (function object)

    11.1.1 函数对象的支持
    1. template<typename Iter, typename Callable>
    2. void foreach (Iter current, Iter end, Callable op)
    3. {
    4. while (current != end) { //as long as not reached the end
    5. op(*current); // call passed operator for current element
    6. ++current; // and move iterator to next element
    7. }
    8. }
    9. #include #include
    10. #include "foreach.hpp"
    11. // a function to call:
    12. void func(int i)
    13. {
    14. std::cout << "func() called for: " << i << ’\n’;
    15. }
    16. // a function object type (for objects that can be used as functions):
    17. class FuncObj {
    18. public:
    19. void operator() (int i) const { //Note: const member function
    20. std::cout << "FuncObj::op() called for: " << i << ’\n’;
    21. }
    22. };
    23. int main()
    24. {
    25. std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    26. foreach(primes.begin(), primes.end(), // range
    27. func); // function as callable (decays to pointer)
    28. foreach(primes.begin(), primes.end(), // range
    29. &func); // function pointer as callable
    30. foreach(primes.begin(), primes.end(), // range
    31. FuncObj()); // function object as callable
    32. foreach(primes.begin(), primes.end(), // range
    33. [] (int i) { //lambda as callable
    34. std::cout << "lambda called for: " << i << ’\n’;
    35. });
    36. }

    详细看一下以上各种情况:

     当把函数名当作函数参数传递时,并不是传递函数本体,而是传递其指针或者引用。和 数组情况类似(参见 7.4 节),在按值传递时,函数参数退化为指针,如果参数类型是 模板参数,那么类型会被推断为指向函数的指针。 和数组一样,按引用传递的函数的类型不会 decay。但是函数类型不能真正用 const 限 制。如果将 foreach()的最后一个参数的类型声明为 Callable const &,const 会被省略。 (通常而言,在主流 C++代码中很少会用到函数的引用。)

     在第二个调用中,函数指针被显式传递(传递了一个函数名的地址)。这和第一中调用 方式相同(函数名会隐式的 decay 成指针),但是相对而言会更清楚一些。

     如果传递的是仿函数,就是将一个类的对象当作可调用对象进行传递。通过一个 class 类型进行调用通常等效于调用了它的 operator()。因此下面这样的调用: op(*current);

    会被转换成: op.operator()(*current);

    注意在定义 operator()的时候最好将其定义成 const 成员函数。否则当一些框架或者库 不希望该调用会改变被传递对象的状态时,会遇到很不容易 debug 的 error。

     Lambda 表达式会产生仿函数(也称闭包),因此它与仿函数(重载了 operator()的类) 的情况没有不同。不过 Lambda 引入仿函数的方法更为简便,因此它们从 C++11 开始变 得很常见。 有意思的是,以[]开始的 lambdas(没有捕获)会产生一个向函数指针进行转换的运算 符。但是它从来不会被当作 surrogate call function,因为它的匹配情况总是比常规闭包 的 operator()要差。

    11.1.2 处理成员函数以及额外的参数

    在以上例子中漏掉了另一种可以被调用的实体:成员函数。这是因为在调用一个非静态成员 函数的时候需要像下面这样指出对象:object.memfunc(...)或者 ptr->memfunc(...),这和常规 情况下的直接调用方式不同:func(...)。

    从 C++17 开始,标准库提供了一个工具:std::invlke(),它非常方便的统一了上面 的成员函数情况和常规函数情况,这样就可以用同一种方式调用所有的可调用对象。:

    1. #include
    2. #include
    3. template<typename Iter, typename Callable, typename… Args>
    4. void foreach (Iter current, Iter end, Callable op, Args const&…args)
    5. {
    6. while (current != end) { //as long as not reached the end of the
    7. elements
    8. std::invoke(op, //call passed callable with
    9. args…, //any additional args
    10. *current); // and the current element
    11. ++current;
    12. }
    13. }
    14. #include
    15. #include
    16. #include
    17. #include "foreachinvoke.hpp"
    18. // a class with a member function that shall be called
    19. class MyClass {
    20. public:
    21. void memfunc(int i) const {
    22. std::cout << "MyClass::memfunc() called for: " << i << ’
    23. \n’;
    24. }
    25. };
    26. int main()
    27. {
    28. std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    29. // pass lambda as callable and an additional argument:
    30. foreach(primes.begin(), primes.end(), //elements for 2nd arg of
    31. lambda
    32. [](std::string const& prefix, int i) { //lambda to call
    33. std::cout << prefix << i << ’\n’;
    34. },
    35. "- value:"); //1st arg of lambda
    36. // call obj.memfunc() for/with each elements in primes passed as
    37. argument
    38. MyClass obj;
    39. foreach(primes.begin(), primes.end(), //elements used as args
    40. &MyClass::memfunc, //member function to call
    41. obj); // object to call memfunc() for
    42. }

    第一次调用 foreach()时,第四个参数被作为 lambda 函数的第一个参数传递给 lambda,而 vector 中的元素被作为第二个参数传递给 lambda。第二次调用中,第三个参数 memfunc() 被第四个参数 obj 调用。

    11.1.3 函数调用的包装

    Std::invoke()的一个常规用法是封装一个单独的函数调用。此时可以通过完美转发可调 用对象以及被传递的参数来支持移动语义:

    1. #include // for std::invoke()
    2. #include // for std::forward()
    3. template<typename Callable, typename… Args>
    4. decltype(auto) call(Callable&& op, Args&&… args)
    5. {
    6. return std::invoke(std::forward(op), //passed callable with
    7. std::forward(args)…); // any additional args
    8. }

    为了能够返回引用(比如 std::ostream&),需要使用 decltype(auto)而不是 auto:

    主要还是因为auto作为返回值,会导致类型退化,decltype(auto) 避免了这种退化。

      auto (可有 cv 限定符)一定会推导出返回类型为对象类型。并且应用数组到指针、函数到指针隐式转换。
      auto 加上 & 或 && (可有 cv 限定符)一定会推导出返回类型为引用类型。
      decltype(auto) 可以推导出对象类型,也可以推导出引用类型。具体取决于 decltype 应用到 return 语句中表达式的结果。

    decltype(auto)(在 C++14 中引入)是一个占位符类型,它根据相关表达式决定了变量、返回值、或者模板实参的类型。

    如果你想暂时的将 std::invoke()的返回值存储在一个变量中,并在做了某些别的事情后将其 返回(比如处理该返回值或者记录当前调用的结束),也必须将该临时变量声明为decltype(auto)类型:

    1. decltype(auto) ret{std::invoke(std::forward(op),
    2. std::forward(args)…)}; …
    3. return ret;

    如果可调用对象的返回值是 void, 那么将 ret 初始化为 decltype(auto)是不可以的,这是因为 void 是不完整类型。

    分别实现 void 和非 void 的情况:

    1. #include // for std::invoke()
    2. #include // for std::forward()
    3. #include // for std::is_same<> and
    4. invoke_result<>
    5. template<typename Callable, typename… Args>
    6. decltype(auto) call(Callable&& op, Args&&… args)
    7. {
    8. if constexpr(std::is_same_vinvoke_result_t
    9. Args…>, void>) {// return type is void:
    10. std::invoke(std::forward(op),
    11. std::forward(args)…); …
    12. return;
    13. } else {
    14. // return type is not void:
    15. decltype(auto) ret{std::invoke(std::forward(op),
    16. std::forward(args)…)}; …
    17. return ret;
    18. }
    19. }

    11.2 其他一些实现泛型库的工具

    11.2.1 类型萃取

    使用类型萃取的时候需要额外小心:其行为可能和程序员的预期不同。比如:

    std::remove_const_t<int const&> // 

    这里由于引用不是 const 类型的(虽然你不可以改变它),这个操作不会有任何效果。

    这样,删除引用和删除 const 的顺序就很重要了:

    1. std::remove_const_tremove_reference_t<int const&>> // int
    2. std::remove_reference_tremove_const_t<int const&>> // int const

    另一种方法是,直接调用:

    std::decay_t<int const&> // yields int

    但是这同样会让裸数组和函数类型退化为相应的指针类型。

    当然还有一些类型萃取的使用是有要求的。这些要求不被满足的话,其行为将是未定义的。 比如:

    1. make_unsigned_t<int> // unsigned int
    2. make_unsigned_t<int const&> // undefined behavior

    某些情况下,结果可能会让你很意外。比如:

    1. add_rvalue_reference_t<int const> // int const&&
    2. add_rvalue_reference_t<int const&> // int const& (lvalueref remains lvalue-ref)

    这里我们期望 add_rvalue_reference 总是能够返回一个右值引用,但是 C++中的引用塌缩 (reference-collapsing rules,参见 15.6.1 节)会令左值引用和右值引用的组合返回一个左值 引用。

    11.2.2 std::addressoff()

    函数模板 std::addressof<>()会返回一个对象或者函数的准确地址。即使一个对象重载了运算 符&也是这样。虽然后者中的情况很少遇到,但是也会发生(比如在智能指针中)。因此, 如果需要获得任意类型的对象的地址,那么推荐使用 addressof():

    1. template<typename T>
    2. void f (T&& x)
    3. {
    4. auto p = &x; // might fail with overloaded operator &
    5. auto q = std::addressof(x); // works even with overloaded operator
    6. & …
    7. }
    11.2.3 std::declval()

    函数模板 std::declval()可以被用作某一类型的对象的引用的占位符。该函数模板没有定义, 因此不能被调用(也不会创建对象)。因此它只能被用作不会被计算的操作数(比如 decltype 和 sizeof)。也因此,在不创建对象的情况下,依然可以假设有相应类型的可用对象。

    比如在如下例子中,会基于模板参数 T1 和 T2 推断出返回类型 RT:

    1. #include
    2. template<typename T1, typename T2,
    3. typename RT = std::decay_t<decltype(true ? std::declval() :
    4. std::declval())>>
    5. RT max (T1 a, T2 b)
    6. {
    7. return b < a ? a : b;
    8. }

    为了避免在调用运算符?:的时候不得不去调用 T1 和 T2 的(默认)构造函数,这里使用了 std::declval,这样可以在不创建对象的情况下“使用”它们。不过该方式只能在不会做真正 的计算时(比如 decltype)使用。

    不要忘了使用 std::decay<>来确保返回类型不会是一个引用,因为 std::declval<>本身返回的 是右值引用。否则,类似 max(1,2)这样的调用将会返回一个 int&&类型。

    11.3 完美转发临时变量

    使用转发引用(forwarding reference)以及 std::forward<> 来完美转发泛型参数:

    1. template<typename T>
    2. void f (T&& t) // t is forwarding reference
    3. {
    4. g(std::forward(t)); // perfectly forward passed argument t to g()
    5. }

    但是某些情况下,在泛型代码中我们需要转发一些不是通过参数传递进来的数据。此时我们 可以使用 auto &&创建一个可以被转发的变量。比如,假设我们需要相继的调用 get()和 set() 两个函数,并且需要将 get()的返回值完美的转发给 set():

    1. template<typename T>void foo(T x)
    2. {
    3. set(get(x));
    4. }

    假设以后我们需要更新代码对 get()的返回值进行某些操作,可以通过将 get()的返回值存储 在一个被声明为 auto &&的变量中实现:

    1. template<typename T>
    2. void foo(T x)
    3. {
    4. auto&& val = get(x); …
    5. // perfectly forward the return value of get() to set():
    6. set(std::forward<decltype(val)>(val));
    7. }

    这样可以避免对中间变量的多余拷贝。

    11.4 作为模板参数的引用

    1. #include
    2. template<typename T>
    3. void tmplParamIsReference(T) {
    4. std::cout << "T is reference: " << std::is_reference_v << '\n';
    5. }
    6. int main()
    7. {
    8. std::cout << std::boolalpha;
    9. int i;
    10. int& r = i;
    11. tmplParamIsReference(i); // false
    12. tmplParamIsReference(r); // false
    13. tmplParamIsReference<int&>(i); // true
    14. tmplParamIsReference<int&>(r); // true
    15. }

    即使传递给 tmplParamIsReference()的参数是一个引用变量,T 依然会被推断为被引用的类型。

    为什么tmplParamIsReference(r); // false?

    (因为对于引用变量 v,表达式 v 的类型是被引用的类型,表达式(expression)的类型永远 不可能是引用类型)。--不明白

    个人理解:模板参数按值传递进行类别推导,引用被去除。

    显示指定 T 的类型化为引用类型。这样做可能 会触发错误或者不可预知的行为。考虑如下例子:

    1. template<typename T, T Z = T{}>
    2. class RefMem {
    3. private:
    4. T zero;
    5. public:
    6. RefMem() : zero{Z} {
    7. }
    8. };
    9. int null = 0;
    10. int main()
    11. {
    12. RefMem<int> rm1, rm2;
    13. rm1 = rm2; // OK
    14. RefMem<int&> rm3; // ERROR: invalid default value for N
    15. RefMem<int&, 0> rm4; // ERROR: invalid default value for N extern
    16. int null;
    17. RefMem<int&,null> rm5, rm6;
    18. rm5 = rm6; // ERROR: operator= is deleted due to reference member
    19. }

    用 int 实例化该模板会 获得预期的行为。但是如果尝试用引用对其进行实例化的话,情况就有点复杂了:

     非模板参数的默认初始化不在可行。

     不再能够直接用 0 来初始化非参数模板参数。

     最让人意外的是,赋值运算符也不再可用,因为对于具有非 static 引用成员的类,其默 赋值运算符会被删除掉。(赋值运算符的默认实现是逐成员赋值,而引用成员是不能被重新绑定的。因此,如果一个类有非静态引用成员,编译器会自动删除其默认赋值运算符,防止意外的引用重新绑定。)

    而且将引用类型用于非类型模板参数同样会变的复杂和危险。考虑如下例子:

    1. #include
    2. #include
    3. template<typename T, int& SZ> // Note: size is reference
    4. class Arr {
    5. private:
    6. std::vector elems;
    7. public:
    8. Arr() : elems(SZ) { //use current SZ as initial vector size
    9. }
    10. void print() const {
    11. for (int i=0; i//loop over SZ elements
    12. std::cout << elems[i] << ’ ’;
    13. }
    14. }
    15. };
    16. int size = 10;
    17. int main()
    18. {
    19. Arr<int&,size> y; // compile-time ERROR deep in the code of class
    20. std::vector<>
    21. Arr<int,size> x; // initializes internal vector with 10 elements
    22. x.print(); // OK
    23. size += 100; // OOPS: modifies SZ in Arr<>
    24. x.print(); // run-time ERROR: invalid memory access: loops over 120 elements
    25. }

    基于这一原因,C++标准库在某些情况下制定了很特殊的规则和限制。比如:

     在模板参数被用引用类型实例化的情况下,为了依然能够正常使用赋值运算符, std::pair<>和 std::tuple<>都没有使用默认的赋值运算符,而是做了单独的定义。比如:

    1. namespace std {
    2. template<typename T1, typename T2>
    3. struct pair {
    4. T1 first;
    5. T2 second; …
    6. // default copy/move constructors are OK even with references:
    7. pair(pair const&) = default;
    8. pair(pair&&) = default; …
    9. // but assignment operator have to be defined to be available with
    10. references:
    11. pair& operator=(pair const& p);
    12. pair& operator=(pair&& p) noexcept(…); …
    13. };
    14. }

     由于这些副作用可能导致的复杂性,在 C++17 中用引用类型实例化标准库模板 std::optional<>和 std::variant<>的过程看上去有些古怪:

    为了禁止用引用类型进行实例化,一个简单的 static_assert 就够了:

    1. template<typename T>
    2. class optional
    3. {
    4. static_assert(!std::is_reference::value, "Invalid
    5. instantiation of optional for references"); …
    6. };

    11.5 推迟计算(Defer Evaluation)

    在实现模板的过程中,有时候需要面对是否需要考虑不完整类型(参见 10.3.1 节)的问题。

    该 class 可以被用于不完整类型。这很有用,比如可以让其成员指向其自身的 类型。 

    1. template<typename T>
    2. class Cont {
    3. private:
    4. T* elems;
    5. public:
    6. };
    7. struct Node
    8. {
    9. std::string value;
    10. Cont next; // only possible if Cont accepts incomplete types
    11. };

    编译运行成功。 

    1. template<typename T>
    2. class Cont {
    3. private:
    4. T* elems;
    5. public:
    6. typename
    7. std::conditional::value, T&&,
    8. T& >::type foo();
    9. };
    10. struct Node
    11. {
    12. std::string value;
    13. Cont next; // only possible if Cont accepts incomplete types
    14. };

    编译报错:
    错误    C2139    “Node”: 未定义的类不允许作为编译器内部类型特征“__is_constructible”的参数   

    这里通过使用 std::conditional来决定 foo()的返回类型是 T&&还是 T&。决策标准 是看模板参数 T 是否支持 move 语义。

    问题在于 std::is_move_constructible 要求其参数必须是完整类型。

    使用这 种类型的 foo(),struct node 的声明就会报错。 

    1. template<typename T>
    2. class Cont {
    3. private:
    4. T* elems;
    5. public:
    6. template<typename D = T>
    7. typename
    8. std::conditional::value, T&&,
    9. T&>::type foo();
    10. };
    11. struct Node
    12. {
    13. std::string value;
    14. Cont next; // only possible if Cont accepts incomplete types
    15. };

     编译运行成功。

    为了解决这一问题,需要使用一个成员模板代替现有 foo()的定义,这样就可以将 std::is_move_constructible 的计算推迟到 foo()的实例化阶段:

    其实就是利用“模板只有在被调用时才会被实例化”和“两阶段编译检查”的特性。

    现在,类型萃取依赖于模板参数 D(默认值是 T),并且编译器会一直等到 foo()被以完整类 型(比如 Node)为参数调用时,才会对类型萃取部分进行计算(此时 Node 是一个完整类 型,其只有在定义时才是非完整类型)。

    11.6 在写泛型库时需要考虑的事情

    1. 在模板中使用转发引用来转发数值(参见 6.1 节)。

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

    如果数值不依赖于模板参数,就使 用 auto &&(参见 11.3)。

    1. template<typename T>
    2. void foo(T x)
    3. {
    4. auto&& val = get(x); …
    5. // perfectly forward the return value of get() to set():
    6. set(std::forward<decltype(val)>(val));
    7. }

     2.如果一个参数被声明为转发引用,并且传递给它一个左值的话,那么模板参数会被推断 为引用类型。

     3.在需要一个依赖于模板参数的对象的地址的时候,最好使用 std::addressof()来获取地址, 这样能避免因为对象被绑定到一个重载了 operator &的类型而导致的意外情况。

     4.对于成员函数,需要确保它们不会比预定义的 copy/move 构造函数或者赋值运算符更 能匹配某个调用。

     5.如果模板参数可能是字符串常量,并且不是被按值传递的,那么请考虑使用 std::decay。

    1. template<typename T1, typename T2>
    2. constexpr pair<typename decay::type, typename
    3. decay::type>
    4. make_pair (T1&& a, T2&& b)
    5. {
    6. return pair<typename decay::type, typename
    7. decay::type>(forward(a), forward(b));
    8. }

    6. 如果你有被用于输出或者即用于输入也用于输出的、依赖于模板参数的调用参数,请为 可能的、const 类型的模板参数做好准备。

    如果想禁止向非 const 应用传递 const 对象,有如下选择:

    使用 static_assert 触发一个编译期错误:

    通过使用 std::enable_if<>(参见 6.3 节)禁用该情况下的模板:

    或者是在 concepts 被支持之后,通过 concepts 来禁用该模板(参见 6.5 节以及附录 E):

    1. template<typename T>
    2. requires !std::is_const_v
    3. void outR (T& arg) {
    4. ……
    5. }

    7. 请为将引用用于模板参数的副作用做好准备(参见 11.4 节)。尤其是在你需要确保返 回类型不会是引用的时候(参见 7.5 节)。

     8.请为将不完整类型用于嵌套式数据结构这一类情况做好准备(参见 11.5 节)。

     9.为所有数组类型进行重载,而不仅仅是 T[SZ](参见 5.4 节)

    11.7 总结

     可以将函数,函数指针,函数对象,仿函数和 lambdas 作为可调用对象(callables)传 递给模板。

     如果需要为一个 class 重载 operator(),那么就将其声明为 const 的(除非该调用会修改 它的状态)。

     通过使用 std::invoke(),可以实现能够处理所有类型的、可调用对象(包含成员函数) 的代码。  使用 decltype(auto)来完美转发返回值。

     类型萃取是可以检查类型的属性和功能的类型函数。  当在模板中需要一个对象的地址时,使用 std::addressof().

     在不经过表达式计算的情况下,可以通过使用 std::declval()创建特定类型的值。

     在泛型代码中,如果一个对象不依赖于模板参数,那么就使用 auto&&来完美转发它。

     可以通过模板来延迟表达式的计算(这样可以在 class 模板中支持不完整类型)

  • 相关阅读:
    JS前端开发框架常用的有哪些?
    docker 容器
    Web前端大作业 体育主题足球网页制作 足球梅西HTML网页设计制作 dreamweaver学生网页源代码
    揭示GPU上的批处理策略
    ES 中时间日期类型 “yyyy-MM-dd HHmmss” 的完全避坑指南
    升级ios16后iphone无法识别SIM?一招解决这个问题!
    动态规划之 铺砖问题
    Coredump
    手动实现简单限流函数
    突遇暴雨,怎样远程连接服务器
  • 原文地址:https://blog.csdn.net/qq_52758467/article/details/132889899