Skip to content

Latest commit

 

History

History
286 lines (245 loc) · 10.4 KB

ch21.md

File metadata and controls

286 lines (245 loc) · 10.4 KB

Chapter21 类型特征扩展

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

21.1 类型特征后缀_v

自从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

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 是否是聚合体类型:

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)

类型特征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<>还 会考虑值类型。也就是说:

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

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

例如,如下定义:

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

类型特征invoke_result<>

  • 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类型类征(几乎所有的返回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");