C++17扩展了类型特征(标准类型函数)的通用能力并引入了一些新的类型特征。
自从C++17起,你可以对所有返回值的类型特征使用后缀_v
(就像你可以为所有返回类型的类型特征使用_t
一样)。
例如,对于任何类型T
,你现在可以写:
std::is_const_v<T> // 自从C++17起
来代替:
std::is_const<T>::value // 自从C++11起
这适用于所有返回值的类型特征。方法是为每一个标准类型特征定义一个相应的新的变量模板。例如:
namespace std {
template<typename T>
constexpr bool is_const_v = is_const<T>::value;
}
这样一个类型特征可以也用做运行时的条件表达式:
if (std::is_signed_v<char>) {
...
}
然而,因为这些类型特征是在编译期求值,所以你也可以在编译期使用它们
(例如,在一个编译期if
语句中):
if constexpr (std::is_signed_v<char>) {
...
}
另一个应用是用来支持不同的模板实例化:
// 类C<T>的主模板
template<typename T, bool = std::is_pointer_v<T>>
class C {
...
};
// 指针类型的偏特化版本:
template<typename T>
class C<T, true> {
...
};
这里,类C
为指针类型提供了一个偏特化版本。
后缀_v
也可以用于返回非bool类型的类型特征,例如std::extent<>
,
返回原生数组的某一个维度的大小:
int a[5][7];
std::cout << std::extent_v<decltype(a)> << '\n'; // 打印出5
std::cout << std::extent_v<decltype(a), 1> << '\n'; // 打印出7
表新的类型特征列出了C++17引入的新类型特征。
特征 | 效果 |
---|---|
is_aggregate<T> |
是否是聚合体类型 |
is_swappable<T> |
该类型是否能调用swap() |
is_nothrow_swappable<T> |
该类型是否能调用swap() 并且该操作不会抛出异常 |
is_swappable_with<T1, T2> |
特定值类型的这两种类型是否能调用swap() |
is_nothrow_swappable_with<T1, T2> |
特定值类型的这两种类型是否能调用swap() 并且该操作不会抛出异常 |
has_unique_object_representations<T> |
是否该类型的两个值相等的对象在内存中的表示也一样 |
is_invocable<T, Args...> |
该类型是否可以用 Args... 调用 |
is_nothrow_invocable<T, Args...> |
该类型是否可以用 Args... 调用,并且该操作不会抛出异常 |
is_invocable_r<RT, T, Args...> |
该类型是否可以用 Args... 调用并返回 RT 类型 |
is_nothrow_invocable_r<RT, T, Args...> |
该类型是否可以用 Args... 调用并返回 RT 类型且不会抛出异常 |
invoke_result<T, Args...> |
用 Args... 作为实参进行调用会返回的类型 |
conjunction<B...> |
对bool特征 B... 进行逻辑与运算 |
disjunction<B...> |
对bool特征 B... 进行逻辑或运算 |
negation<B> |
对bool特征B进行非运算 |
is_execution_policy<T> |
是否是执行策略类型 |
另外,is_literal_type<>
和result_of<>
自从C++17起被废弃。
下面的段落将详细讨论这些特征。
std::is_aggregate<T>::value
返回 T 是否是聚合体类型:
template<typename T>
struct D : std::string, std::complex<T> {
std::string data;
};
D<float> s{{"hello"}, {4.5, 6.7}, "world"}; // 自从C++17起OK
std::cout << std::is_aggregate<decltype(s)>::value; // 输出:1(true)
std::is_swappable<T>::value
std::is_nothrow_swappable<T>::value
std::is_swappable_with<T1, T2>::value
std::is_nothrow_swappable_with<T1, T2>::value
在以下情况下返回true:
- 类型
T
的左值可以被交换,或者 - 类型
T1
和T2
的值类型可以交换
(使用nothrow
形式时还要保证不会抛出异常)。
注意is_swappable<>
或is_nothrow_swappable<>
检查你是否
可以交换某个指定类型的值(检查该类型的左值)。
相反,is_swappable_with<>
和is_nothrow_swappable_with<>
还
会考虑值类型。也就是说:
is_swappable_v<int> // 返回true
等价于
is_swappable_with_v<int&, int&> // 返回true(和上边等价)
而:
is_swappable_with_v<int, int> // 返回false
将会返回false,因为你不能调用std::swap(42, 77)
。
例如:
is_swappable_v<std::string> // 返回true
is_swappable_v<std::string&> // 返回true
is_swappable_v<std::string&&> // 返回true
is_swappable_v<void> // 返回false
is_swappable_v<void*> // 返回true
is_swappable_v<char[]> // 返回false
is_swappable_with_v<std::string, std::string> // 返回false
is_swappable_with_v<std::string&, std::string&> // 返回true
is_swappable_with_v<std::string&&, std::string&&> // 返回false
std::has_unique_object_representations<T>::value
当任意两个值相同的类型T
的对象在内存中的表示都相同时返回true。
也就是说,两个相同的值在内存中总是有相同的字节序列。
有这种属性的对象可以通过对字节序列哈希来得到对象的哈希值 (不用担心某些不参与比较的位可能不同的情况)。
std::is_invocable<T, Args...>::value
std::is_nothrow_invocable<T, Args...>::value
std::is_invocable_r<Ret, T, Args...>::value
std::is_nothrow_invocable_r<Ret, T, Args...>::value
当你能以Args...
为实参调用T
类型的对象并且返回值可以转换为Ret
类型(并且保证不抛出异常)时返回true。
也就是说,我们可以使用这些特征来测试我们是否可以用Args...
为实参调用
(直接调用或者通过std::invoke()
)T
类型的可调用对象并返回Ret
类型的值。
例如,如下定义:
struct C {
bool operator() (int) const {
return true;
}
};
会导致下列结果:
std::is_invocable<C>::value // false
std::is_invocable<C, int>::value // true
std::is_invocable<int*>::value // false
std::is_invocable<int(*)()>::value // true
std::is_invocable_r<bool, C, int>::value // true
std::is_invocable_r<int, C, long>::value // true
std::is_invocable_r<void, C, int>::value // true
std::is_invocable_r<char*, C, int>::value // false
std::is_invocable_r<long, int(*)(int)>::value // false
std::is_invocable_r<long, int(*)(int), int>::value // true
std::is_invocable_r<long, int(*)(int), double>::value // true
std::invoke_result<T, Args...>::type
返回当使用实参Args...
调用T
类型的对象时会返回的类型。
也就是说,我们可以使用这个特征来获知如果使用Args...
调用T
类型的对象时
将会返回的类型。
这个类型特征替换了result_of<>
,后者现在不应该再使用。
例如:
std::string foo(int);
using T1 = std::invoke_result_t<decltype(foo), int>; // T1是std::string
struct ABC {
virtual ~ABC() = 0;
void operator() (int) const {
}
};
using T2 = typename std::invoke_result<ABC, int>::type; // T2是void
表组合其他类型特征的类型特征列出了对bool类型类征(几乎所有的返回bool类型值的标准类型特征) 进行逻辑组合的类型特征。
特征 | 效果 |
---|---|
conjunction<B...> |
对bool特征 B... 进行逻辑 与 运算 |
disjunction<B...> |
对bool特征 B... 进行逻辑 或 运算 |
negation<B> |
对bool特征B进行 非 运算 |
它们的一大应用就是通过组合现有类型特征来定义新的类型特征。 例如,你可以轻松的定义一个检查某个类型是否是“指针”(原生指针,成员函数指针,或者空指针)的特征:
template<typename T>
struct IsPtr : std::disjunction<std::is_null_pointer<T>,
std::is_member_pointer<T>,
std::is_pointer<T>> {
};
现在我们在一个编译期if
语句中使用这个新的特征:
template<typename T>
void foo(T x)
{
if constexpr(IsPtr<T>) {
... // 处理是指针的情况
}
else {
... // 处理不是指针的情况
}
}
作为另一个例子,我们可以定义一个检查指定类型是否是整数或者枚举但又不是bool的类型特征:
template<typename T>
struct IsIntegralOrEnum : std::conjunction<std::disjunction<std::is_integral<T>,
std::is_enum<T>>,
std::negation<std::is_same<T, bool>>> {
};
这里,类似于计算
(is_integral<T> || is_enum<T>) && !is_same<T, bool>
这么写的一个好处是std::conjunction<>
和std::disjunction<>
会 短路求值 bool表达式,这意味着当 conjunction 出现第一个false
或者
disjunction 出现第一个true
时就会停止计算。
这节省了编译时间,甚至因为短路求值可以在某些情况下使原本无效的代码变得有效。
例如,如果像下面这样使用不完全类型:
struct X {
X(int); // 从int转换而来
};
struct Y; // 不完全类型
下面的静态断言会失败,因为对于不完全类型is_constructible
会陷入未定义行为
(尽管有些编译器接受这样的代码):
// 未定义行为
static_assert(std::is_constructible<X, int>{} || std::is_constructible<Y, int>{},
"can't init X or Y from int");
下面使用std::disjunction
的替代版保证不会失败,
因为当is_constructible<X, int>
返回true
后求值就会停止:
// OK:
static_assert(std::disjunction<std::is_constructible<X, int>,
std::is_constructible<Y, int>>{},
"can't init X or Y from int");