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>;
::std::remove_cvref_t<T>stripsconst,volatileand&:int const&becomesint. This is the trait you want almost every time a template stores a type, becausevector<T const &>is not a container of references, it is an error.::std::decay_t<T>does the same plus array-to-pointer and function-to-pointer. Use it when mimicking by-value parameter passing (Ch15.5); useremove_cvref_teverywhere else, becausedecay_tthrows away information you usually want.::std::conditional_t<cond, A, B>isiffor types.::std::common_type_t<A, B>is the type both convert to, which is what a binary operation should return.::std::underlying_type_t<E>is the integer behind anenum class(Ch2.16).
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
- Traits answer questions; concepts name the answers;
if constexpracts on them. Do not confuse the three. - Use
remove_cvref_tby default,decay_tonly when imitating by-value deduction. - Prefer the
_v/_tforms; the::value/::typeforms are for reading old code. - Never test a trait that the algorithm does not use: it rejects types for no reason and it costs compile time.