Let’s start with a more generic question, what is a trait? What does the word trait mean?
According to the Cambridge Dictionary, a trait is “a particular characteristic that can produce a particular type of behaviour”. Or simply “a characteristic, especially of a personality”.
It’s important to start our quest with the generic meaning, as many of us are native English speakers and having a clear understanding of the word trait helps us to have a better understanding also on the programming concept.
In C++, we can think about type traits as properties of a type. The <type_traits>
header was an addition introduced by C++11. Type traits can be used in template metaprogramming to inspect or even to modify the properties of a type.
As we saw in the C++ concepts series, you’d often need the information of what kind of types are accepted by a template, what types are supported by certain operations. While concepts are much superior in terms of expressiveness or usability, with type traits you could already introduce compile-time conditions on what should be accepted as valid code and what not.
Though type traits can help with even more. With their help, you can also add or remove the const
specifier, or you can turn a pointer or a reference into a value and so on.
As already mentioned, the library is used in the context of template metaprogramming, so everything happens at compile time.
Show me a type trait!
In the concepts series, I already mentioned std::is_integral
(in fact, I used std::is_integral_v
, more on that later.) Like other type traits, std::is_integral
is after all an integral_constant
that has a static value
member and some type information.
Let’s see how std::is_integral
is implemented, by looking at the GCC implementation. While it might be different for other implementations, it should give you the basic idea.
1
2
3
4
template<typename _Tp>
struct is_integral
: public __is_integral_helper<typename remove_cv<_Tp>::type>::type
{ };
At first glance, we can see that it uses a certain __is_integral_helper
that is also a template and it takes the passed in type without its const
or volatile
qualifier if any.
Now let’s have a look at __is_integral_helper
.
Due to the limitations of this blog post and also due to common sense I won’t enumerate all the specialisations of the template _is_integral_helper
, I’ll only show here three just to give you the idea.
1
2
3
4
5
6
7
8
9
10
11
template<typename>
struct __is_integral_helper
: public false_type { };
template<>
struct __is_integral_helper<bool>
: public true_type { };
template<>
struct __is_integral_helper<int>
: public true_type { };
As we can observe, the default implementation of __is_integral_helper
is a false_type
. Meaning that in case you call std::is_integral
with a random type, that type will be handed over to __is_integral_helper
and it will be a false type that has the value of false
, therefore the check fails.
For any type that should return true
for the is_integral
checks, __is_integral_helper
should be specialized and it should inherit from true_type
.
In order to close this circle, let’s see how true_type
and false_type
are implemented.
1
2
3
4
5
/// The type used as a compile-time boolean with true value.
typedef integral_constant<bool, true> true_type;
/// The type used as a compile-time boolean with false value.
typedef integral_constant<bool, false> false_type;
As we can see, they are simple aliased integral_constants
.
As the last step, let’s see how std::integral_constant
is built. (I omit the #if, etc. directives on purpose)
1
2
3
4
5
6
7
8
9
template<typename _Tp, _Tp __v>
struct integral_constant
{
static constexpr _Tp value = __v;
typedef _Tp value_type;
typedef integral_constant<_Tp, __v> type;
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; }
};
So integral_constant
takes two template parameters. It takes a type _Tp
and a value __v
of the just previously introduced type _Tp
.
__v
will be accessible as the static value
member, while the type _Tp
itself can be referred to as the value_type
nested type. With the type
typedef you can access the type itself.
So true_type
is an integral_constant
where type
is bool
and value is true
.
In case you have std::is_integral<int>
- through multiple layers - it inherits from true_type
, std::is_integral<int>::value
is true
. For any type T
, std::is_integral<T>::type
is bool.
How to make your type satisfy a type trait
We’ve just seen how std::is_integral
is implemented. Capitalizing on that we might think that if you have a class MyInt
then having it an integral type only means that we simply have to write such code (I omit the problem of references and cv qualifications for the sake of simplicity):
1
2
template<>
struct std::is_integral<MyInt> : public std::integral_constant<bool, true> {};
This is exactly what I proposed in the Write your own concepts article.
If you read attentively, probably you pointed out that I used the auxiliary “might” and it’s not incidental.
I learned that having such a specialization results in undefined behaviour according to the standard [meta.type.synop (1)]:
“The behavior of a program that adds specializations for any of the templates defined in this subclause is undefined unless otherwise specified.”
What is in that subsection? Go look for a draft standard (here is one) if you don’t have access to a paid version. It’s a very long list, and I tell you std::is_integral
is part of it. In fact, all the primary or composite type categories are in there.
Why?
As Howard Hinnant, the father of <chrono>
explained on StackOverflow “for any given type T, exactly one of the primary type categories has a value member that evaluates to true.” If a type satisfies std::is_floating_point
then we can safely assume that std::is_class
will evaluate to false. As soon as we are allowed to add specializations, we cannot rely on this.
1
2
3
4
5
6
7
8
9
10
11
#include <type_traits>
class MyInt {};
template<>
struct std::is_integral<MyInt> : public std::integral_constant<bool, true> {};
int main() {
static_assert(std::is_integral<MyInt>::value, "MyInt is not integral types");
static_assert(std::is_class<MyInt>::value, "MyInt is not integral types");
}
In the above example, MyInt
breaks the explained assumption and this is in fact undefined behaviour, something you should not rely on.
And the above example shows us another reason, why such specializations cannot be considered a good practice. Developers cannot be trusted that much. We either made a mistake or simply lied by making MyInt
an integral type as it doesn’t behave at all like an integral.
This basically means that you cannot make your type satisfy a type trait in most cases. (As mentioned the traits that are not allowed to be specialized are listed in the standard).
Conclusion
Today, we learned what type traits are, how they are implemented and we also saw that we cannot explicitly say about a user-defined type that it belongs to a primary or composite type category. Next week, we’ll see how we can use type traits.