Что такое traits?
В данной статье я попытаюсь рассказать, что такое traits. Будут рассмотрены некоторые примеры применения traits, которые будут заключаться как в использовании traits в нашем коде, так и в возможных способах расширения стандартной библиотеки C++, которая тоже использует traits. Также будут рассмотрены возможные проблемы, которые могут возникнуть при расширении стандартной библиотеки C++. Эта статья написана для программистов на C++, которые уже неплохо владеют самим языком, его основными конструкциями. В частности, необходимо знание, что такое шаблоны(templates) и желателен опыт их использования. Также очень желательно знание стандартной библиотеки C++, так как многие примеры будут посвящены именно ей.
Итак, приступим. Думаю, начать стоит с перевода термина traits. Обычно его переводят как "свойства". Но traits реализуются классом, поэтому обычно употребляется термин "класс свойств". Следует заметить, что свойства также можно реализовать с помощью структуры, так как в C++ это практически аналоги. Далее я буду использовать термин класс, хотя все сказанное будет в той же мере относиться к структурам.
Теперь следует дать определение свойств. Натан Майерс, разработавший метод использования свойств, предложил такое определение:
Класс свойств — это класс, используемый вместо параметров шаблона. В качестве класса он объединяет полезные типы и константы; как шаблон, он является средством для обеспечения того "дополнительного уровня косвенности", который решает все проблемы программного обеспечения.
// используем параметры по умолчанию
vector<int> vec1; // эквивалентно: vector<int, const int&, int&, const int&>
// переопределяем один из параметров по умолчанию
// обратите на второй аргумент шаблона(не ссылка, а передача по значению)
vector<int, const int> vec2; // эквивалентно: vector<int, const int, int&, const int&>
// переопределяем один из параметров по умолчанию
vector<char, const char> vec3; // // эквивалентно: vector<char, const char, char&, const char&>
// используем параметры по умолчанию
vector<char> vec4; // эквивалентно: vector<char, const char&, char&, const char&>
template <typename T,
typename traits = elem_traits<T> > // свойство по умолчанию
class vector <
// .
public:
typedef T value_type;
typedef typename traits::arg_type arg_type;
typedef typename traits::reference reference;
typedef typename traits::const_reference const_reference;
// используется аргумент-свойство по умолчанию
vector<int> vec1; // эквивалентно: vector<int, elem_traits<int> >
// тогда:
// arg_type = const int&
// reference = int&
// const_reference = const int&
// используется аргумент-свойство по умолчанию
vector<char> vec1; // эквивалентно: vector<char, elem_traits<char> >
// тогда:
// arg_type = const char&
// reference = char&
// const_reference = const char&
<type_traits>
Определяет шаблоны для констант времени компиляции, которые предоставляют сведения о свойствах аргументов типа или создают преобразованные типы.
Синтаксис
Remarks
Классы и шаблоны в <type_traits> используются для поддержки вывода типов, классификации и преобразования во время компиляции. Они также используются для обнаружения ошибок, связанных с типом, и для оптимизации универсального кода. Унарные признаки типов описывают свойство типа, признаки двоичного типа описывают связь между типами, а признаки преобразования изменяют свойство типа.
Вспомогательный класс integral_constant и его специализации true_type шаблона и false_type образуют базовые классы для предикатов типов. Предикат типа — это шаблон, принимающий один или несколько аргументов типа. Если предикат типа имеет значение true, он является общедоступным или косвенным образом из true_type. Если предикат типа содержит значение false, он является общедоступным или косвенным образом от false_type.
Модификатор типа или признак преобразования — это шаблон, принимающий один или несколько аргументов шаблона и имеющий один член ( type ), который является синонимом для измененного типа.
Шаблоны псевдонимов
Для упрощения выражений признаков типа предоставляются шаблоны typename some_trait<T>::type псевдонимов, где some_trait — это имя шаблона класса. Например, add_const имеет шаблон псевдонима для своего типа, add_const_t , определяемого следующим образом.
Это предоставленные псевдонимы для type членов:
add_const_t
add_cv_t
add_lvalue_reference_t
add_pointer_t
add_rvalue_reference_t
add_volatile_t
aligned_storage_t
aligned_union_t \
common_type_t
conditional_t
decay_t
enable_if_t
invoke_result_t
make_signed_t
make_unsigned_t
remove_all_extents_t \
remove_const_t
remove_cv_t
remove_extent_t
remove_pointer_t
remove_reference_t
remove_volatile_t
result_of_t
underlying_type_t \
What are type traits in C++?
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.
Exit fullscreen mode
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.
Exit fullscreen mode
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.
Exit fullscreen mode
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)
Exit fullscreen mode
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):
Exit fullscreen mode
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.
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.
Exit fullscreen mode
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.