Chapter21 类型特征扩展

C++17扩展了类型特征(标准类型函数)的通用能力并引入了一些新的类型特征。

21.1 类型特征后缀_v

自从C++17起,你可以对所有返回值的类型特征使用后缀_v (就像你可以为所有返回类型的类型特征使用_t一样)。 例如,对于任何类型T,你现在可以写:

  1. std::is_const_v<T> // 自从C++17起

来代替:

  1. std::is_const<T>::value // 自从C++11起

这适用于所有返回值的类型特征。方法是为每一个标准类型特征定义一个相应的新的变量模板。例如:

  1. namespace std {
  2. template<typename T>
  3. constexpr bool is_const_v = is_const<T>::value;
  4. }

这样一个类型特征可以也用做运行时的条件表达式:

  1. if (std::is_signed_v<char>) {
  2. ...
  3. }

然而,因为这些类型特征是在编译期求值,所以你也可以在编译期使用它们 (例如,在一个编译期if语句中):

  1. if constexpr (std::is_signed_v<char>) {
  2. ...
  3. }

另一个应用是用来支持不同的模板实例化:

  1. // 类C<T>的主模板
  2. template<typename T, bool = std::is_pointer_v<T>>
  3. class C {
  4. ...
  5. };
  6. // 指针类型的偏特化版本:
  7. template<typename T>
  8. class C<T, true> {
  9. ...
  10. };

这里,类C为指针类型提供了一个偏特化版本。

后缀_v也可以用于返回非bool类型的类型特征,例如std::extent<>, 返回原生数组的某一个维度的大小:

  1. int a[5][7];
  2. std::cout << std::extent_v<decltype(a)> << '\n'; // 打印出5
  3. std::cout << std::extent_v<decltype(a), 1> << '\n'; // 打印出7

21.2 新的类型特征

表新的类型特征列出了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起被废弃。 下面的段落将详细讨论这些特征。

类型特征is_aggregate<>

  • std::is_aggregate<T>::value

返回 T 是否是聚合体类型:

  1. template<typename T>
  2. struct D : std::string, std::complex<T> {
  3. std::string data;
  4. };
  5. D<float> s{{"hello"}, {4.5, 6.7}, "world"}; // 自从C++17起OK
  6. std::cout << std::is_aggregate<decltype(s)>::value; // 输出:1(true)

类型特征is_swappable<>is_swappable_with<>

  • 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的左值可以被交换,或者
  • 类型T1T2的值类型可以交换

(使用nothrow形式时还要保证不会抛出异常)。

注意is_swappable<>is_nothrow_swappable<>检查你是否 可以交换某个指定类型的值(检查该类型的左值)。 相反,is_swappable_with<>is_nothrow_swappable_with<>还 会考虑值类型。也就是说:

  1. is_swappable_v<int> // 返回true

等价于

  1. is_swappable_with_v<int&, int&> // 返回true(和上边等价)

而:

  1. is_swappable_with_v<int, int> // 返回false

将会返回false,因为你不能调用std::swap(42, 77)

例如:

  1. is_swappable_v<std::string> // 返回true
  2. is_swappable_v<std::string&> // 返回true
  3. is_swappable_v<std::string&&> // 返回true
  4. is_swappable_v<void> // 返回false
  5. is_swappable_v<void*> // 返回true
  6. is_swappable_v<char[]> // 返回false
  7. is_swappable_with_v<std::string, std::string> // 返回false
  8. is_swappable_with_v<std::string&, std::string&> // 返回true
  9. is_swappable_with_v<std::string&&, std::string&&> // 返回false

类型特征has_unique_object_representations<>

  • std::has_unique_object_representations<T>::value

当任意两个值相同的类型T的对象在内存中的表示都相同时返回true。 也就是说,两个相同的值在内存中总是有相同的字节序列。

有这种属性的对象可以通过对字节序列哈希来得到对象的哈希值 (不用担心某些不参与比较的位可能不同的情况)。

类型特征is_invocable<>is_invocable_r<>

  • 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类型的值。

例如,如下定义:

  1. struct C {
  2. bool operator() (int) const {
  3. return true;
  4. }
  5. };

会导致下列结果:

  1. std::is_invocable<C>::value // false
  2. std::is_invocable<C, int>::value // true
  3. std::is_invocable<int*>::value // false
  4. std::is_invocable<int(*)()>::value // true
  5. std::is_invocable_r<bool, C, int>::value // true
  6. std::is_invocable_r<int, C, long>::value // true
  7. std::is_invocable_r<void, C, int>::value // true
  8. std::is_invocable_r<char*, C, int>::value // false
  9. std::is_invocable_r<long, int(*)(int)>::value // false
  10. std::is_invocable_r<long, int(*)(int), int>::value // true
  11. std::is_invocable_r<long, int(*)(int), double>::value // true

类型特征invoke_result<>

  • std::invoke_result<T, Args...>::type

返回当使用实参Args...调用T类型的对象时会返回的类型。 也就是说,我们可以使用这个特征来获知如果使用Args...调用T类型的对象时 将会返回的类型。

这个类型特征替换了result_of<>,后者现在不应该再使用。

例如:

  1. std::string foo(int);
  2. using T1 = std::invoke_result_t<decltype(foo), int>; // T1是std::string
  3. struct ABC {
  4. virtual ~ABC() = 0;
  5. void operator() (int) const {
  6. }
  7. };
  8. using T2 = typename std::invoke_result<ABC, int>::type; // T2是void

bool类型特征的逻辑操作

表组合其他类型特征的类型特征列出了对bool类型类征(几乎所有的返回bool类型值的标准类型特征) 进行逻辑组合的类型特征。

特征 效果
conjunction<B...> 对bool特征 B… 进行逻辑 运算
disjunction<B...> 对bool特征 B… 进行逻辑 运算
negation<B> 对bool特征B进行 运算

它们的一大应用就是通过组合现有类型特征来定义新的类型特征。 例如,你可以轻松的定义一个检查某个类型是否是“指针”(原生指针,成员函数指针,或者空指针)的特征:

  1. template<typename T>
  2. struct IsPtr : std::disjunction<std::is_null_pointer<T>,
  3. std::is_member_pointer<T>,
  4. std::is_pointer<T>> {
  5. };

现在我们在一个编译期if语句中使用这个新的特征:

  1. template<typename T>
  2. void foo(T x)
  3. {
  4. if constexpr(IsPtr<T>) {
  5. ... // 处理是指针的情况
  6. }
  7. else {
  8. ... // 处理不是指针的情况
  9. }
  10. }

作为另一个例子,我们可以定义一个检查指定类型是否是整数或者枚举但又不是bool的类型特征:

  1. template<typename T>
  2. struct IsIntegralOrEnum : std::conjunction<std::disjunction<std::is_integral<T>,
  3. std::is_enum<T>>,
  4. std::negation<std::is_same<T, bool>>> {
  5. };

这里,类似于计算

  1. (is_integral<T> || is_enum<T>) && !is_same<T, bool>

这么写的一个好处是std::conjunction<>std::disjunction<>短路求值 bool表达式,这意味着当 conjunction 出现第一个false或者 disjunction 出现第一个true时就会停止计算。 这节省了编译时间,甚至因为短路求值可以在某些情况下使原本无效的代码变得有效。

例如,如果像下面这样使用不完全类型:

  1. struct X {
  2. X(int); // 从int转换而来
  3. };
  4. struct Y; // 不完全类型

下面的静态断言会失败,因为对于不完全类型is_constructible会陷入未定义行为 (尽管有些编译器接受这样的代码):

  1. // 未定义行为
  2. static_assert(std::is_constructible<X, int>{} || std::is_constructible<Y, int>{},
  3. "can't init X or Y from int");

下面使用std::disjunction的替代版保证不会失败, 因为当is_constructible<X, int>返回true后求值就会停止:

  1. // OK:
  2. static_assert(std::disjunction<std::is_constructible<X, int>,
  3. std::is_constructible<Y, int>>{},
  4. "can't init X or Y from int");