Generic programming is a technique to generalize certain components of a software system by decreasing the dependency to the type information. This also brings an advantage on code resuablity and maintainance.
However, sometimes, the implementation details might differ for specific types within the same algorithm/class. Also, some types can be better handled in terms of efficiency. Therefore, more information is required for those types.
This is where Traits come into the picture. The Traits are mechanisms that can carry or transform specific informations about types. This is a compile-time mechanism which eliminates all run-time complexities and error handlings.
To get this mechanism, #include <type_traits>.
Basically, it is achieved by partial or full specialization of a generic struct which is designed to carry a specific information about types. Type Traits are used in two ways:
- Carrying type information
- Changing type information
Before jumping into the details, the definition of integral_constant is crucial. This trait is mostly used as base class for the value-based type traits [1].
template <typename T, T val>
struct integral_constant
{
using value_type = T;
using type = integral_constant;
static constexpr T value = val;
};
template<bool val>
using bool_constant = integral_constant<bool, val>;
using true_type = integral_constant<bool, true>; // bool_constant<true>;
using false_type = integral_constant<bool, false>; // bool_constant<false>;
Each compiler vendors have different implementations for type traits. The approach presented here is only one of them, NOT the only one.
Type traits classes can provide an answer for the following questions or more:
- is a type void?
- is type A same of type B?
- is a type pointer or reference?
- is a type floating point?
etc. It is always better to explain through the code.
// None of the types are void
template<typename T>
struct is_void : public false_type{};
// except the void itself
// full specialization of is_void<T>
template<>
struct is_void<void> : public true_type{};
// Two given types are always different
template <typename T, typename U>
struct is_same : public false_type{};
// Except the given types are the same
template<typename T>
struct is_same<T, T> : public true_type{};
// std::is_pointer
template<typename T>
struct is_pointer : public false_type{};
template<typename T>
struct is_pointer<T*> : public true_type{};
// std::is_reference
template<typename T>
struct is_reference : public false_type{};
template<typename T>
using is_reference_v = typename is_reference<T>::value;
template<typename T>
struct is_reference<T&> : public true_type{};
template<typename T>
using is_reference_v = typename is_reference<T&>::value;
template<typename T>
struct is_reference<T&&> : public true_type{};
template<typename T>
using is_reference_v = typename is_reference<T&&>::value;
template<typename T>
using is_float = typename is_same<T, float>;
// OR
template<typename T>
struct is_float
{
static constexpr bool value = is_same<T, float>::value;
};
// OR
template <typename T>
struct is_float : public false_type{};
template<>
struct is_float<float> : public true_type{};
template<>
struct is_float<double> : public true_type{};
template<>
struct is_float<long double> : public true_type{};
Type Traits mechanism provides great flexibility during an algorithm/data structure design when some type specialization is required for efficiency. The very typical example for the usage is Optimized Copy in the STL [1]. In general, default implementation of the copy algorithm works well for the most of the types. However, some types can be copied in a more efficient way using memcopy and this puts some requirements on the types. These requirements can only be checked with Type Traits.
A simple example can be found at [3]. A bit more complex example is discussed at [2].
Type Traits can also be used to transform the type by changing its information. The standard library provides several of them.
- Removing const qualifier from a type
- Add/remove reference
- Add lvalue reference
// Remove const from a given type
template<typename T>
struct remove_const {using type = T;};
template<typename T>
struct remove_const<const T> {using type = T;};
template<typename T>
struct remove_reference {using type = T;};
template<typename T>
struct remove_reference<T&> {using type = T;};
template<typename T>
struct remove_reference<T&&> {using type = T;};
// This implementation is not the best but still works
template<typename...>
struct void_t{};
// void& and void&& is not allowed so leave as is
template<typename T, typename = void>
struct add_lvalue_reference {using type = T;};
// for all the types other then void (SFINAE)
template<typename T>
struct add_lvalue_reference<T, void_t<T&> > {using type = T&;};
Each class that performs type transformation holds a alias (or typedef) member type which yields to the result of the transformation.
struct A{void print() const { std::cout << "--> A <--\n";}};
struct B{void print() const { std::cout << "--> B <--\n";}};
struct C{void print() const { std::cout << "--> C <--\n";}};
template<bool val>
using the_bool_constant = std::integral_constant<bool, val>;
template<typename T>
struct is_A_type : public the_bool_constant<false>{};
template<>
struct is_A_type<A> : public the_bool_constant<true>{};
template<typename T>
struct is_B_type : public the_bool_constant<false>{};
template<>
struct is_B_type<B> : public the_bool_constant<true>{};
template<typename T>
struct is_C_type : public the_bool_constant<false>{};
template<>
struct is_C_type<C> : public the_bool_constant<true>{};
// VIA parameter
template<typename T>
void print_via_func_par1(const T& val,
typename std::enable_if<is_A_type<T>::value, T>::type* = 0)
{
val.print();
}
template<typename T>
void print_via_func_par2(const T& val,
typename std::enable_if<is_A_type<T>::value>::type* = 0)
{
val.print();
}
template<typename T>
void print_via_func_par3(const T& val,
std::enable_if_t<is_A_type<T>::value>* = 0)
{
val.print();
}
// Via non-type template parameter
template<class T,
typename std::enable_if<is_B_type<T>::value, bool>::type = true>
void print_via_non_type_templ_par1(const T& val)
{
val.print();
}
template<class T, std::enable_if_t<is_B_type<T>::value, bool> = true>
void print_via_non_type_templ_par2(const T& val)
{
val.print();
}
// via type template parameter
template<class T, typename = typename std::enable_if<is_C_type<T>::value, T>::type >
void print_via_type_templ_par1(const T& val)
{
val.print();
}
template<class T, typename = typename std::enable_if<is_C_type<T>::value>::type >
void print_via_type_templ_par2(const T& val)
{
val.print();
}
template<class T, typename = std::enable_if_t<is_C_type<T>::value> >
void print_via_type_templ_par3(const T& val)
{
val.print();
}
void test1(){
A a_obj;
print_via_func_par1(a_obj);
print_via_func_par2(a_obj);
print_via_func_par3(a_obj);
B b_obj;
print_via_non_type_templ_par1(b_obj);
print_via_non_type_templ_par2(b_obj);
C c_obj;
print_via_type_templ_par1(c_obj);
print_via_type_templ_par2(c_obj);
print_via_type_templ_par3(c_obj);
}
- Boost libraries
- C++ Type Traits - Dr.Dobbs
- A quick primer on type traits in modern C++
- How to Implement std::declval, add_lvalue_reference, add_rvalue_reference, std::void_t
- An introduction to C++ Traits
- C++ short stories: type traits, concepts, and type constraints
- Simplify your type traits with C++14 variable templates
- Using C++ Trait Classes for Scientific Computing
- [Effective Modern C++ - Scott Meyers]