Chapter33 编写泛型代码的改进

C++17引入了很多辅助工具来帮助实现泛型代码和库。

注意我们已经在类型特征扩展一章中介绍了一些新的类型特征。

33.1 std::invoke<>()

新的工具std::invoke<>()是一个新的辅助函数, 它被用于编写调用一个可调用对象的代码,可调用对象包括函数、lambda、 有operator()的函数对象、成员函数。

这里有一个辅助函数的例子演示怎么使用它:

  1. #include <utility> // for std::invoke()
  2. #include <functional> // for std::forward()
  3. template<typename Callable, typename... Args>
  4. void call(Callable&& op, Args&&... args)
  5. {
  6. ...
  7. std::invoke(std::forward<Callable>(op), // 调用传入的可调用对象
  8. std::forward<Args>(args)...); // 以传入的其他参数为参数
  9. }

你传递给call()的第一个参数,将会按照如下方式使用剩余的参数进行调用:

  • 如果可调用对象是一个成员函数的指针,将使用剩余参数中的第一个参数作为调用成员函数的对象, 所有其他参数被用作调用的参数。
  • 否则,可调用对象会把剩余参数用作自己的参数进行调用。

例如:

  1. #include "invoke.hpp"
  2. #include <iostream>
  3. #include <vector>
  4. void print(const std::vector<int>& coll)
  5. {
  6. std::cout << "elems: ";
  7. for (const auto& elem : coll) {
  8. std::cout << elem << ' ';
  9. }
  10. std::cout << '\n';
  11. }
  12. int main()
  13. {
  14. std::vector<int> vals{0, 8, 15, 42, 13, -1, 0};
  15. call([&vals] {
  16. std::cout << "size: " << vals.size() << '\n';
  17. });
  18. call(print, vals);
  19. call(&decltype(vals)::pop_back, vals);
  20. call(print, vals);
  21. call(&decltype(vals)::clear, vals);
  22. call(print, vals);
  23. }

注意在不指明要调用哪个版本的情况下调用重载函数将导致错误:

  1. call(&decltype(vals)::resize, vals, 5); // ERROR:resize()被重载了
  2. call<void(decltype(vals)::*)(std::size_t)>(&decltype(vals)::resize, vals, 5); // OK

还要注意调用函数模板需要显式实例化。如果print()是一个模板:

  1. template<typename T>
  2. void print(const T& coll)
  3. {
  4. std::cout << "elems: ";
  5. for (const auto& elem : coll) {
  6. std::cout << elem << ' ';
  7. }
  8. std::cout << '\n';
  9. }

那么当你将它传给call时必须显式指明模板参数:

  1. call(print, vals); // ERROR:不能推导出模板参数T
  2. call(print<std::vector<int>>, vals); // OK

最后,注意根据移动语义的规则,转发一个调用的结果需要使用decltype(auto)完美返回 返回值到调用者:

  1. template<typename Callable, typename... Args>
  2. decltype(auto) call(Callable&& op, Args&&.. args)
  3. {
  4. return std::invoke(std::forward<Callable>(op), // 调用传入的可调用对象
  5. std::forward<Args>(args)...); // 以传入的其他参数为参数
  6. }

33.2 std::bool_constant<>

如果一个特征返回bool值,那么它们现在使用了新的模板别名bool_constant<>

  1. namespace std {
  2. template<bool B>
  3. using bool_constant = integral_constant<bool, B>; // 自从C++17起
  4. using true_type = bool_constant<true>;
  5. using false_type = bool_constant<false>;
  6. }

在C++17之前,你必须直接使用integral_constant<>,这意味着true_typefalse_type按照如下方式定义:

  1. namespace std {
  2. using true_type = integral_constant<bool, true>;
  3. using false_type = integral_constant<bool, false>;
  4. }

bool特征仍然是在满足特定属性时继承std::true_type, 在不满足时继承std::false_type。例如:

  1. // 主模板:T不是void类型时
  2. template<typename T>
  3. struct IsVoid : std::false_type {
  4. };
  5. // 为类型void的特化
  6. template<>
  7. struct IsVoid<void> : std::true_type {
  8. };

然而,你现在可以通过派生自bool_constant<>来定义自己的类型特征, 只需要制定相应的编译期表达式作为一个bool条件。例如:

  1. template<typename T>
  2. struct IsLargerThanInt : std::bool_constant<(sizeof(T) > sizeof(int))> {
  3. }

之后你可以使用这样一个特征来在编译期判断一个类型是否大于int

  1. template<typename T>
  2. void foo(T x)
  3. {
  4. if constexpr(IsLargerThanInt<T>::value) {
  5. ...
  6. }
  7. }

通过添加相应的内联变量:

  1. template<typename T>
  2. inline static constexpr auto IsLargerThanInt_v = IsLargerThanInt<T>::value;

你可以把这个特征的使用缩短为如下形式:

  1. template<typename T>
  2. void foo(T x)
  3. {
  4. if constexpr(IsLargerThanInt_v<T>) {
  5. ...
  6. }
  7. }

作为另一个例子,我们可以定义一个如下的特征来粗略的检查 一个类型T的移动构造函数是否保证不抛出异常:

  1. template<typename T>
  2. struct IsNothrowMoveConsructibleT : std::bool_constant<noexcept(T(std::declval<T>()))> {
  3. };

33.3 std::void_t<>

还有一个很小但很有用的辅助定义类型特征的工具在C++17中被标准化了:std::void_t<>。 它简单的按照如下形式定义:

  1. namespace std {
  2. template<typename...> using void_t = void;
  3. }

也就是说,对于任何可变模板参数列表它都会返回void。 如果我们只想在参数列表中处理类型时这会很有用。

它的主要应用就是当定义新的类型特征时检查条件。 下面的例子演示了它的应用:

  1. #include <utility> // for declval<>
  2. #include <type_traits> // for true_type, false_type, void_t
  3. // 主模板:
  4. template<typename, typename = std::void_t<>>
  5. struct HasVarious : std::false_type {
  6. };
  7. // 部分特化(may be SFINAE'd away):
  8. template<typename T>
  9. struct HasVarious<T, std::void_t<decltype(std::declval<T>().begin()),
  10. typename T::difference_type,
  11. typename T::iterator>>
  12. : std::true_type {
  13. };

这里,我们定义了一个新的类型特征HasVariousT<>,它会检查如下三个条件:

  • 该类型有成员函数begin()吗?
  • 该类型有类型成员difference_type吗?
  • 该类型有类型成员iterator吗?

只有当对于类型T所有相应的条件都有效时才会使用部分特化版本。 在这种情况下,它的特化程度比主模板更高所以会使用它, 并且因为我们从std::true_type继承, 所以该特征的值将是true

  1. if constexpr (HasVarious<T>::value) {
  2. ...
  3. }

如果任何表达式导致无效代码(即T没有begin()、或者 没有类型成员difference_type、或者没有类型成员iterator), 部分特化版会 SFINAE’d away , 这意味着根据 代换失败不是错误(substitution failure is not an error) 规则它会被忽略。 之后,只有主模板可以使用,它派生自std::false_type,如果检查它的值 会返回false

使用这种方式,你可以使用std::void_t来轻易的定义其他检查一个或多个条件的特征, 这些条件包括是否存在某个成员或操作或者某个成员或操作的能力。 参见HasDelete<>获取另一个例子。