Ch15.1: Type Traits: What the Compiler Knows About a Type

中文(简体)

1. Facts About Types, As Values

Ch2.7 used sizeof(T), a compile-time number describing a type. <type_traits> is the rest of that idea: a library of compile-time questions and compile-time transformations, all answered without running anything.

Every trait comes in one of two shapes:

Shape Example You get
a boolean constant ::std::is_integral_v<int> true, usable in static_assert, if constexpr, or a concept
a type ::std::remove_cvref_t<int const &> int, usable as a template argument or a variable type

The _v suffix means “the value” and _t means “the type”. Without the suffix you get an old-style helper struct (::std::is_integral<int>::value), which is only worth knowing so you can read pre-C++17 code.

The chapter's subject starts here because a template cannot decide anything until it knows what the type allows. Traits and concepts are the two ways of asking, and Ch15.3 needs this page's vocabulary to talk about requirements.

2. The Traits You Will Actually Use

There are over a hundred traits in the standard; about ten carry most of the weight.

Trait Question
is_integral_v<T>, is_floating_point_v<T>, is_pointer_v<T> is it this kind of type?
is_same_v<A, B> are these the same type, exactly (no conversions)?
is_trivially_copyable_v<T> can it be copied byte by byte, like an int?
is_nothrow_copy_constructible_v<T>, is_nothrow_move_constructible_v<T> does copying/moving promise not to fail?
is_nothrow_destructible_v<T> does the destructor promise not to fail? (It must — Ch14.2.)
is_nothrow_swappable_v<T> can swap be used in a noexcept context? (Ch14.7)
is_copy_constructible_v<T>, is_move_assignable_v<T> does that operation exist at all?
is_base_of_v<B, D>, is_convertible_v<A, B> inheritance and conversion questions (Chapter 18)

Notice what the nothrow rows have in common: they are exactly the facts Chapter 14 needed. my_posix_file's move constructor is noexcept not because the author felt like it but because ::std::is_nothrow_move_constructible_v is what lets ::fast_io::vector<my_posix_file> grow without copying.

3. The Traits You Will Actually Apply

The transformations produce a type, and they are how generic code spells “the same type, cleaned up”:


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>;

A warning that pays for itself: the _t form of ::std::conditional is lazy about the condition, not about its arguments. If A is an invalid type, conditional_t still has to name it. When both branches cannot be named, use if constexpr inside a function instead (Ch15.8).

4. Traits Under Concepts

A concept is a constexpr bool with a name, so traits go straight into one:


template <typename T>
concept cheap_to_copy = ::std::is_trivially_copyable_v<T> &&
						::std::is_nothrow_copy_constructible_v<T>;

Two structs tell the two halves apart. silent has noexcept copy operations; loud's copy constructor is throws. Both are copyable, only one is cheap_to_copy, and the compiler knows it before either is instantiated anywhere:


struct silent
{
	silent() noexcept = default;
	silent(silent const &) noexcept = default;
	~silent() noexcept = default;
};

struct loud
{
	loud() throws {}
	loud(loud const &other) throws {}
};

This is the standard library's own technique. ::std::vector decides between “reallocate by moving” and “reallocate by copying” by asking exactly these traits; fast_io's allocators do the same thing with their throws_on_allocation_failure flag, which is why ::fast_io::vector<T> is throws or not depending on the allocator rather than on the element type.

5. The Whole Program


#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);
	}
}

Output:


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. Rules of Thumb