Template Metaprogramming---Type Traits
The Aims
- How to implement and how to use
- Exploration of the standard set of type traits
- Focus on techniques for implementing type traits
- Remove some of the mystique that still surrounds template metaprogramming.
- Practical advice from a regular user.
- So you can more readily use the standard set and implement your own when
needed.
What is Meta-Programming
- In general, when programs treat programs as data
- Could be other programs or itself
- Could be at “compile time” or “run time”
- We will discuss compile time metaprogramming in C++
- Wide array of current techniques, but still considered a niche
- This two-part tutorial helps shed light on a very few essential ideoms
Why Care About Metaprogramming (and type traits in particular)
- Each new standard library employs more metaprogramming techniques
- Some requirements are impossible without advanced techniques(e.g., std::optional)
- Many third party libraries, not just Boost
- Tools and idioms have become well developed, no longer black magic, limited to STL and Boost.
- All C++ programmers should understand the basics.
- Any library developer should understand a good bit more
- C++20 - concepts and independent requires expressions
It’s kind of a paradigm shift, there are a lot of things that make metaprogramming more look like
regular functional programming.
Meta-Functions
- A meta-function is not a function but a class/struct
- Meta-functions are not part of the language and have no formal language support
- They exist as an idiomatic use of existing language features
- Their use is not enforced by the language
- Their use is dictated by convention
- C++ community has created common “standard” conventions
The Definition of meta-function
- Technically, a class with zero+ template parameters and zero+ return types and values
- Convention is that a meta-function should return one thing, like a regular function
- Convention was developed over time, so plenty of existing examples that do not follow this convention
- More modern meta-functions do follow this convention
Return From a Meta-Function
- Expose a public value “value”
1 | template <typename T> |
- Expose a public type “type”
1 | template <typename T> |
Meta-functions yield back some types to you.
Value Meta-functions
- Simple regular function: identity
This is a very simple regular function.
1 | int int_identity(int x) { reutrn x; } |
- The Simple Meta-Function: identity
1 | template <int X> |
- Generic Identity Function
1 | template <typename T> |
- Generic Identity Meta-Function
1 | template <typename T, T Value> |
- Generic Identity Meta-Function (C++17)
Template accepts non-type template parameter since C++17.
1 | template <auto X> |
- Two forms of Sum Function
1 | template <typename X, typename Y> |
Type Meta-Functions
- Type meta-functions just like a workhorse (especially will the advent of constexpr) which manipulate
types. - “Returns” a type.
This is a type meta-function demo
1 | template <typename T> |
C++20 introduces std::type_identity
Calling Type Meta-Functions
1 | ValueIdentity<42>::value; |
Typename Dance
1 | typename TypeIdentity<T>::type; |
Convenience Calling Conventions
- Value meta-functions use variable templates ending with “_v”.
1 |
|
- Type Meta-Functions use alias templates ending with “_t”.
Typename Dance
.
1 | template <typename T> |
These calling conventions are easier to use. But each one must be explicitly handwritten.
- A meta-convention to get around that which I may get to if time for bonus material.
Type Traits
Some Useful Meta-Functions
std::integral_constant
A very useful meta-function. It allows us to wrap a constant with its type.
1 | template <class T, T v> |
std::bool_constant
This is Convenient helpers.
1 | template <bool B> |
true_type
and false_type
are going to be meta-functions. They are called nullary meta-functions
because they have no parameters.
1 | true_type::value; |
Standard Type Trait Requirements
Cpp17 Unary Type Trait
Cpp20 introduces very different meta-programming techniques.
For a unary type trait in the standard library which is what we’re got which is what we are talking
about. Unary type trait in the standard library it has a class template of one template type
argument
Cpp17UnaryTypeTrait
- Class Template
- One template type argument*
- Cpp17DefaultConstructible
- Cpp17CopyConstructible
- Publicly and unambiguously derived from a specialization of
std::integral_constant
.
All the unary type traits have to derive fromintegral_constant
. - The member names of the base characteristic shall not be hidden and shall be unambiguously available
Basically, this means if you inherit from it you can’t hide any of that stuff, you got to let all
that stuff be available publicly.
Cpp17BinaryTypeTrait
This is an exactly same thing with Cpp17UnaryTypeTrait
except Cpp17BinaryTypeTrait has two
template type argument*.
Cpp17TransformationTrait
- Class Template
- One template type argument*
- Define a publicly asccessible nested type name
type
. - No default/copy constructible requirement
- No inheritance requirement
Specialization
is_void (Unary Type Trait)
- Value meta-function: is the type void? yields true_type or false_type
Specialization
Primary template: general case
1
2template <typename T>
struct is_void : std::false_type {};Specialization: special case(s)
1
2
3
4
5
6
7
8
9
10// The empty angle brackets mean it's an explicit full
// specialization, and then we take the type that we
// are specializing for. And we put it in right place.
// In this case, we are going to return true and so
// these static_assert.
template<>
struct is_void<void> : std::true_type {};
static_assert(is_void<void>{});
static_assert(not is_void<int>{});Why does is void reutrn true type false type instead of true false values?
The reason because it is a meta-function returning the true type(the actual type of it).
First of all, the is_void
is inherited from integral_constant
. false_type
is just integral
constant bool false. true_type
is just integral_constant
bool true. The standard says that
unary meta-functions must inherit from one of those.
And the reason because if all we did was just return a true value where is a
is_void
is inherit from a true_type
, and true_type
is already having a type.
static_assert(is_void<void>{}
, the curly bracket, that is instantiating one of those things and it
implicit conversion operator to turn it into a true.
- Is
void const
void? - Is
void volatile
void? is_void
is in primary type categories.
Yes, the standard says void
& void const
& void volatile
& void const volatile
are all void
.
cv
stands for const volatile
For any given type T, the result of applying one of these templates to T and to cv T shall yield
the same result.
The definition of is_void
1 | // The primary template |
remove_const (Transformation Trait)
There are three type traits: unary traits/binary traits/transformation traits. remove_const
is
a transformation traits. transformation traits are what they call they are type meta-functions.
- Formal Definition
The member typedef type names the same type as T except that any top-level const-qualifier has
been removed.
The top-level
qualifier, like volatile/const which are attached to the type itself.
1 | remove_const<int> -> int |
- The definition of
remove_const
1 | template <typename T> |
Contains const
so the partial specialization will match.
1 | template <typename T> |
The const is explicitly matched so the part remaining to match with the “T” is int volatile
conditional
This is basically think of it as like an if statement in regular programming. Some conditions it
returns T, else return F.
In this you can read it, if the bool condition is true, return T, else return F.
1 | template <typename T> |
Not all the type traits can be implemented by c++, the compiler has way more information about
the type system and about what’s going on than it is exposed to the programmer through the
language.
Type traits can be implemented by intrinsics, and compiler can be more efficient for intrinsics
processing.
is_union
should be supported by compiler.
Primary Type Categories
There are 14 primary type categories.
1 | is_void is_class |
- All are to have base characteristic of either
true_type
orfalse_type
. - All should yield the same result in light of cv(const volatile) qualifiers.
is_null_pointer
1 | template <typename T> |
is_floating_point
float
/double
/long double
requires 12 specializations.
1 | template <typename T> struct is_floating_point : std::false_type {}; |
is_integral
- Five standard signed integer types:
signed char
,short int
,int
,long int
,long long int
. - Implementation defined extended signed integer types.
- Corresponding, but different, unsigned integer types.
- char, char8_t, char16_t, char32_t, wchar_t.
- bool
- Requires 16 * 4 = 54 specializations.
Meta-Function Abstractions
- We would have reached for this long before now with regular/normal programming.
- Treat meta-function programming like regular programming because, well, that’s what it is.
- Step back to the land of regular functions.
- Pretend we needed to implement these same ideas with strings instead of types.
The regular type of is_void
1 | bool is_void(std::string_view s) { |
A new version of type traits(A Step in the right direction)
1 | std::string_view remove_cv(std::string_view); |
We already have remove_const
, we also need remove_volatile
, compose them to get remove_cv
.
remove_volatile
- Formal Definition
The member typedef type names the same type as T except that any top-level volatile-qualifier has
been remove.
1 | template <typename T> |
remove_cv
- Formal Definition
The member typedef type names the same type as T except that any top-level cv-qualifier has
been removed.
1 | // template <typename T> |
Eg. remove_cv<int const volatile>
1 | // Removing volatile, then const |
Eg. remove_ct_t<int const volatile>
1 | template <typename T> |
is_same
1 | template <typename T1, typename T2> |
Examples
static_assert(not is_same_v<int, unsigned>)
T1 = int, T2 = unsigned, primary template matches. No way to make T to match
specialization.
static_assert(is_same_v<int, int>)
T1 = int, T2 = int, primary template matches, T = int – specialization matches
is_same_raw
This is not standard type traits, but it is kind of useful. Take two types, and remove each
cv qualifiers and then compares them. If the two types are the same after removing these
cv qualifiers, then I’m treat them the same. So this might be helpful considering that
all of our type traits want us to remove both the const and the volatile qualifiers.
1 | template <typename T1, typename T2> |
is_floating_point: redux
This is using alias template.
1 | template <typename T> |
is_integral: redux
1 | template <typename T> |
It might be implemented using parameter pack.
is_type_in_pack
is a meta-function, it takes a type and ti take a list of bunch of other types.
Adn is_type_in_pack
will biscally returned true if that type was anywhere in that list.
1 | template <typename TargetT, typename ...Ts> |
is_array
The definition of is_array
.
1 | template <typename T> |
Some examples.
1 | static_assert(is_array,int[5]>); |
is_pointer
1 | namespace detail { |
is_union
This meta-function is actually impossible to implement without support from the compiler. Both clang
and gcc provide this particular compiler intrinsic to determine if a type is a union.
1 | template <typename T> |
is_class_or_union
What do we know about unions and classes that is unique to those two types?
- They can have members.
- Devise a way to detect if a type can have a member.
- How can you tell if a class has a member?
- The syntax for a pointer-to-member is valid for any class, even without any members.
Eg.
int*
is a valid pointer type, but does not have to point to anything. meta-programming is aimed to
deal with types, not the data.
- int Foo::* is a member pointer type, does not have to point to anything.
1 | // An empty struct, with no members of any kind |
Funciton Overload Resolution
1 | namespace detail { |
1 | static_assert(not is_null_pointer<int>::value); |
Another case.
1 | template <typename T> |
This uses technique called Tag Dispatch
. Tag Dispatch
is where we are creating a type that is
just being used as a tag.
TypeIdentity
takes no space, and it is very efficient to pass these guys around.
SFINAE (Substitution Failure Is Not An Error)
1 | template <typename T> |
is_class
Almost always implemented as compiler intrinsic. Because compiler is much faster dealing with
intrinsics than dealing with even the simplest template stuff. Without the support of compiler, it
is kind of impossible to distinguish between union and non-union class type.
We have is_union
(with help from the compiler)
- The definition is:
1 | namespace detail { |
Implement is_class
using constexpr
.
1 | namespace detail { |
1 | template <typename T> |
- decltype — tells you to pretend that compiler will evaluate this expression, and give me the
result the type that you would get from the evaluated expression. - declval — is there so you can grab a reference to any type. It just gives you a reference
to something as if you had created one as if you had one. So it just declaration, it’s there’s no
implementation.
is_in_pack
1 | // Template declaration, with no definition |
Examples
1 | static_assert(IsInPack<int, double,char,int,float>::value); |
The version of using is_base_of
.
1 | namespace detail { |
Examples
1 | static_assert(IsInPack<int, double,char,int,float>::value); |
is_base_of
If Derived is derived from Base or if both are the same non-union class (in both cases
cv-qualification), provides the member constant value equal to true
. Otherwise value is false
.
The possible definition of is_base_of
is as follows:
1 | namespace detail { |
Learning materials.
- Modern Template Metaprogramming: A Compendium, Part I, Walter E. Brown,
CppCon 2014,
Link: Part I
Part II
References
Template Metaprogramming---Type Traits
https://wtffqbpl.github.io/2022/11/06/Template-Metaprogramming-Type-Traits/