Chapter33 编写泛型代码的改进
C++17引入了很多辅助工具来帮助实现泛型代码和库。
注意我们已经在类型特征扩展一章中介绍了一些新的类型特征。
33.1 std::invoke<>()
新的工具std::invoke<>()是一个新的辅助函数,
它被用于编写调用一个可调用对象的代码,可调用对象包括函数、lambda、
有operator()的函数对象、成员函数。
这里有一个辅助函数的例子演示怎么使用它:
#include <utility> // for std::invoke()#include <functional> // for std::forward()template<typename Callable, typename... Args>void call(Callable&& op, Args&&... args){...std::invoke(std::forward<Callable>(op), // 调用传入的可调用对象std::forward<Args>(args)...); // 以传入的其他参数为参数}
你传递给call()的第一个参数,将会按照如下方式使用剩余的参数进行调用:
- 如果可调用对象是一个成员函数的指针,将使用剩余参数中的第一个参数作为调用成员函数的对象, 所有其他参数被用作调用的参数。
- 否则,可调用对象会把剩余参数用作自己的参数进行调用。
例如:
#include "invoke.hpp"#include <iostream>#include <vector>void print(const std::vector<int>& coll){std::cout << "elems: ";for (const auto& elem : coll) {std::cout << elem << ' ';}std::cout << '\n';}int main(){std::vector<int> vals{0, 8, 15, 42, 13, -1, 0};call([&vals] {std::cout << "size: " << vals.size() << '\n';});call(print, vals);call(&decltype(vals)::pop_back, vals);call(print, vals);call(&decltype(vals)::clear, vals);call(print, vals);}
注意在不指明要调用哪个版本的情况下调用重载函数将导致错误:
call(&decltype(vals)::resize, vals, 5); // ERROR:resize()被重载了call<void(decltype(vals)::*)(std::size_t)>(&decltype(vals)::resize, vals, 5); // OK
还要注意调用函数模板需要显式实例化。如果print()是一个模板:
template<typename T>void print(const T& coll){std::cout << "elems: ";for (const auto& elem : coll) {std::cout << elem << ' ';}std::cout << '\n';}
那么当你将它传给call时必须显式指明模板参数:
call(print, vals); // ERROR:不能推导出模板参数Tcall(print<std::vector<int>>, vals); // OK
最后,注意根据移动语义的规则,转发一个调用的结果需要使用decltype(auto)来
完美返回 返回值到调用者:
template<typename Callable, typename... Args>decltype(auto) call(Callable&& op, Args&&.. args){return std::invoke(std::forward<Callable>(op), // 调用传入的可调用对象std::forward<Args>(args)...); // 以传入的其他参数为参数}
33.2 std::bool_constant<>
如果一个特征返回bool值,那么它们现在使用了新的模板别名bool_constant<>:
namespace std {template<bool B>using bool_constant = integral_constant<bool, B>; // 自从C++17起using true_type = bool_constant<true>;using false_type = bool_constant<false>;}
在C++17之前,你必须直接使用integral_constant<>,这意味着true_type和
false_type按照如下方式定义:
namespace std {using true_type = integral_constant<bool, true>;using false_type = integral_constant<bool, false>;}
bool特征仍然是在满足特定属性时继承std::true_type,
在不满足时继承std::false_type。例如:
// 主模板:T不是void类型时template<typename T>struct IsVoid : std::false_type {};// 为类型void的特化template<>struct IsVoid<void> : std::true_type {};
然而,你现在可以通过派生自bool_constant<>来定义自己的类型特征,
只需要制定相应的编译期表达式作为一个bool条件。例如:
template<typename T>struct IsLargerThanInt : std::bool_constant<(sizeof(T) > sizeof(int))> {}
之后你可以使用这样一个特征来在编译期判断一个类型是否大于int:
template<typename T>void foo(T x){if constexpr(IsLargerThanInt<T>::value) {...}}
通过添加相应的内联变量:
template<typename T>inline static constexpr auto IsLargerThanInt_v = IsLargerThanInt<T>::value;
你可以把这个特征的使用缩短为如下形式:
template<typename T>void foo(T x){if constexpr(IsLargerThanInt_v<T>) {...}}
作为另一个例子,我们可以定义一个如下的特征来粗略的检查
一个类型T的移动构造函数是否保证不抛出异常:
template<typename T>struct IsNothrowMoveConsructibleT : std::bool_constant<noexcept(T(std::declval<T>()))> {};
33.3 std::void_t<>
还有一个很小但很有用的辅助定义类型特征的工具在C++17中被标准化了:std::void_t<>。
它简单的按照如下形式定义:
namespace std {template<typename...> using void_t = void;}
也就是说,对于任何可变模板参数列表它都会返回void。
如果我们只想在参数列表中处理类型时这会很有用。
它的主要应用就是当定义新的类型特征时检查条件。 下面的例子演示了它的应用:
#include <utility> // for declval<>#include <type_traits> // for true_type, false_type, void_t// 主模板:template<typename, typename = std::void_t<>>struct HasVarious : std::false_type {};// 部分特化(may be SFINAE'd away):template<typename T>struct HasVarious<T, std::void_t<decltype(std::declval<T>().begin()),typename T::difference_type,typename T::iterator>>: std::true_type {};
这里,我们定义了一个新的类型特征HasVariousT<>,它会检查如下三个条件:
- 该类型有成员函数
begin()吗? - 该类型有类型成员
difference_type吗? - 该类型有类型成员
iterator吗?
只有当对于类型T所有相应的条件都有效时才会使用部分特化版本。
在这种情况下,它的特化程度比主模板更高所以会使用它,
并且因为我们从std::true_type继承,
所以该特征的值将是true:
if constexpr (HasVarious<T>::value) {...}
如果任何表达式导致无效代码(即T没有begin()、或者
没有类型成员difference_type、或者没有类型成员iterator),
部分特化版会 SFINAE’d away ,
这意味着根据 代换失败不是错误(substitution failure is not an error) 规则它会被忽略。
之后,只有主模板可以使用,它派生自std::false_type,如果检查它的值
会返回false。
使用这种方式,你可以使用std::void_t来轻易的定义其他检查一个或多个条件的特征,
这些条件包括是否存在某个成员或操作或者某个成员或操作的能力。
参见HasDelete<>获取另一个例子。
