C++17引入了很多辅助工具来帮助实现泛型代码和库。
注意我们已经在类型特征扩展一章中介绍了一些新的类型特征。
新的工具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:不能推导出模板参数T
call(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)...); // 以传入的其他参数为参数
}
如果一个特征返回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>()))> {
};
还有一个很小但很有用的辅助定义类型特征的工具在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<>
获取另一个例子。