第15.1章 类型特征:编译器知道哪些关于类型的事
1. 关于类型的事实,作为值
第2.7章用过 sizeof(T)
这个描述类型的编译期数字。<type_traits> 就是那个想法的其余部分:
一整套编译期提出的问题和编译期完成的变换,全部不需要运行任何东西就能回答。
每个特征只有两种形状:
| 形状 | 例子 | 你拿到什么 |
|---|---|---|
| 布尔常量 | ::std::is_integral_v<int> |
true,可用于 static_assert、if constexpr
或概念中 |
| 一个类型 | ::std::remove_cvref_t<int const &> |
int,可用作模板实参或变量类型 |
后缀 _v 表示“值”,_t 表示“类型”。不加后缀得到的是旧式辅助
结构体(::std::is_integral<int>::value),它只值得了解,好让你能读
2020 年以前的代码。
本章的主题从这里开始,因为模板在知道类型允许什么之前,什么都决定不了。问的方式有两种: 类型特征与概念;第15.3章 需要本页的词汇才能谈“要求”。
2. 你真正会用到的特征
标准里有一百多个特征,其中大约十个担起了绝大部分重量。
| 特征 | 问的问题 |
|---|---|
is_integral_v<T>、is_floating_point_v<T>、
is_pointer_v<T> |
它是这一类类型吗? |
is_same_v<A, B> |
这两个类型是否完全相同(不含转换)? |
is_trivially_copyable_v<T> |
能不能像 int 那样按字节拷贝? |
is_nothrow_copy_constructible_v<T>、
is_nothrow_move_constructible_v<T> |
拷贝/移动承诺不失败吗? |
is_nothrow_destructible_v<T> |
析构函数承诺不失败吗?(它必须如此, 第14.2章。) |
is_nothrow_swappable_v<T> |
swap 能否用在 noexcept 语境里?
(第14.7章) |
is_copy_constructible_v<T>、
is_move_assignable_v<T> |
这个操作到底存在吗? |
is_base_of_v<B, D>、is_convertible_v<A, B> |
继承与转换的问题(第18章) |
注意所有 nothrow 那几行的共同点:它们正是
第14章需要的事实。my_posix_file
的移动构造是 noexcept,不是因为作者心情好,而是因为
::std::is_nothrow_move_constructible_v 正是让
::fast_io::vector<my_posix_file> 扩容时不必拷贝的那个条件。
3. 你真正会施加的特征
变换类特征产出一个类型,泛型代码就是用它们来表达“同一个类型,收拾干净之后的样子”:
template <typename T>
using stored_t = ::std::remove_cvref_t<T>;
template <typename T>
using widened_t = ::std::conditional_t<(sizeof(T) < 4), ::std::int_least32_t, T>;
::std::remove_cvref_t<T>去掉const、volatile和&:int const&变成int。模板要存类型时几乎总该用它,因为vector<T const &>不是“引用的容器”,而是一个错误。::std::decay_t<T>在它之上再加数组退化成指针、函数退化成指针。 模拟按值传参时用它(第15.5章); 其他场合一律用remove_cvref_t,因为decay_t会扔掉你通常需要的信息。::std::conditional_t<cond, A, B>是类型层面的if。::std::common_type_t<A, B>是两者都能转换到的那个类型,也正是二元运算该返回的类型。::std::underlying_type_t<E>是enum class背后的整数类型 (第2.16章)。
一个能省下大量时间的警告:::std::conditional 的 _t 形式对“条件”
是惰性的,对两个实参却不惰性。若 A 是一个非法类型,
conditional_t 照样得把它写出来。当两个分支都无法命名时,改用函数里的
if constexpr(第15.8章)。
4. 概念底下的类型特征
概念就是一个有名字的 constexpr bool,所以类型特征可以直接写进去:
template <typename T>
concept cheap_to_copy = ::std::is_trivially_copyable_v<T> &&
::std::is_nothrow_copy_constructible_v<T>;
两个 struct 用来看清这两半。silent 的拷贝操作是
noexcept;loud 的拷贝构造是 throws。
两者都能拷贝,但只有一个是 cheap_to_copy,而编译器在任何实例化之前就知道。
struct silent
{
silent() noexcept = default;
silent(silent const &) noexcept = default;
~silent() noexcept = default;
};
struct loud
{
loud() throws {}
loud(loud const &other) throws {}
};
这正是标准库自己的做法。::std::vector 靠问这些特征,在“移动式重新分配”
与“拷贝式重新分配”之间做决定;fast_io 的分配器则用
throws_on_allocation_failure 标志做同样的事,这也是为什么
::fast_io::vector<T> 是否 throws
取决于分配器而不是元素类型。
5. 完整程序
#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <stdfloat>
#include <type_traits>
struct silent
{
silent() noexcept = default;
silent(silent const &) noexcept = default;
~silent() noexcept = default;
};
struct loud
{
loud() throws {}
loud(loud const &other) throws {}
};
template <typename T>
using stored_t = ::std::remove_cvref_t<T>;
template <typename T>
using widened_t = ::std::conditional_t<(sizeof(T) < 4), ::std::int_least32_t, T>;
template <typename T>
concept cheap_to_copy = ::std::is_trivially_copyable_v<T> &&
::std::is_nothrow_copy_constructible_v<T>;
int main()
{
try
{
::fast_io::io::println("is_integral_v<int>=", ::std::is_integral_v<int>);
::fast_io::io::println("is_same_v<stored_t<int const &>, int>=",
::std::is_same_v<stored_t<int const &>, int>);
::fast_io::io::println("sizeof(widened_t<char>)=", sizeof(widened_t<char>));
::fast_io::io::println("cheap_to_copy<silent>=", cheap_to_copy<silent>);
::fast_io::io::println("cheap_to_copy<loud>=", cheap_to_copy<loud>);
::fast_io::io::println("is_nothrow_copy_constructible_v<loud>=",
::std::is_nothrow_copy_constructible_v<loud>);
::fast_io::io::println("common_type_t<int,float64_t> is float64_t: ",
::std::is_same_v<::std::common_type_t<int, ::std::float64_t>,
::std::float64_t>);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
输出:
is_integral_v<int>=1
is_same_v<stored_t<int const &>, int>=1
sizeof(widened_t<char>)=4
cheap_to_copy<silent>=1
cheap_to_copy<loud>=0
is_nothrow_copy_constructible_v<loud>=0
common_type_t<int,float64_t> is float64_t: 1
6. 经验法则
- 类型特征回答问题;概念给答案起名;
if constexpr依据答案行事。三者不要混。 - 默认用
remove_cvref_t;只有在模拟按值推导时才用decay_t。 - 优先用
_v/_t形式;::value/::type形式是给读老代码用的。 - 绝不测试算法用不到的特征:那只会毫无理由地拒绝类型,还多花编译时间。