第15.1章 类型特征:编译器知道哪些关于类型的事

English

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::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. 经验法则