Type Traits (std::is_integral, std::is_same, etc.)

Type Traits in C++ are a powerful mechanism introduced in the Standard Template Library (STL) that allows the examination and manipulation of types at compile time. They are implemented as template structures or classes and can be used to query properties about types, enabling compile-time decisions and optimizations. Type traits are a key part of template metaprogramming and are typically found in the <type_traits> header.

Some common examples of type traits include:

  • std::is_integral: Checks if a type is an integral type (like int, char, long, etc.).
  • std::is_same: Checks if two types are exactly the same.
  • std::is_floating_point: Checks if a type is a floating-point type (float, double, etc.).
  • std::is_array, std::is_pointer, std::is_reference, etc.

Purpose and Benefits of Type Traits

Type traits are primarily used in template metaprogramming to:

  • Make compile-time decisions based on types.
  • Enable type checking and type transformations.
  • Simplify code by avoiding manual checks for type properties.
  • Implement SFINAE (Substitution Failure Is Not An Error) and enable_if for template specialization or overloading based on types.

Commonly Used Type Traits

Here are some of the most commonly used type traits:

std::is_integral<T>: Determines if a type is an integral type.

#include <type_traits>
#include <iostream>

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_integral<int>::value << "\n";    // true
    std::cout << std::is_integral<float>::value << "\n";  // false
}

std::is_same<T, U>: Determines if two types T and U are the same.

#include <type_traits>
#include <iostream>

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_same<int, int>::value << "\n";   // true
    std::cout << std::is_same<int, double>::value << "\n";  // false
}

std::is_floating_point<T>: Checks if a type is a floating-point type (float, double, or long double).

#include <type_traits>
#include <iostream>

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_floating_point<float>::value << "\n";  // true
    std::cout << std::is_floating_point<int>::value << "\n";    // false
}

std::is_pointer<T>: Determines if a type is a pointer.

#include <type_traits>
#include <iostream>

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_pointer<int*>::value << "\n";   // true
    std::cout << std::is_pointer<int>::value << "\n";    // false
}

std::is_reference<T>: Determines if a type is a reference type (lvalue or rvalue reference).

#include <type_traits>
#include <iostream>

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_reference<int&>::value << "\n";  // true
    std::cout << std::is_reference<int>::value << "\n";   // false
}

std::is_const<T>: Determines if a type is a const-qualified type.

#include <type_traits>
#include <iostream>

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_const<const int>::value << "\n";  // true
    std::cout << std::is_const<int>::value << "\n";        // false
}

    Type Trait Structure

    Type traits are typically implemented as template structs. They have a member ::value which is a constexpr bool that evaluates to true or false.

    Here’s a simplified version of how std::is_integral might be implemented:

    template<typename T>
    struct is_integral {
        static constexpr bool value = false;
    };
    
    template<>
    struct is_integral<int> {
        static constexpr bool value = true;
    };
    
    template<>
    struct is_integral<char> {
        static constexpr bool value = true;
    };
    
    // Other integral types would have similar specializations

    The general case defaults to false, and specific specializations are provided for integral types like int and char to return true.

    Type Traits in Action with enable_if and SFINAE

    Type traits are often used with std::enable_if and SFINAE to enable or disable functions or class templates based on the properties of types.

    Example: std::enable_if

    #include <type_traits>
    #include <iostream>
    
    template <typename T>
    typename std::enable_if<std::is_integral<T>::value, T>::type
    add(T a, T b) {
        return a + b;
    }
    
    template <typename T>
    typename std::enable_if<std::is_floating_point<T>::value, T>::type
    add(T a, T b) {
        return a + b;
    }
    
    int main() {
        std::cout << add(2, 3) << std::endl;          // Calls integral version
        std::cout << add(2.5, 3.1) << std::endl;      // Calls floating-point version
        // std::cout << add("hello", "world") << std::endl;  // Error, not integral or floating-point
    }

    In this example:

    • enable_if is used to enable the first add function only if T is an integral type and the second add function only if T is a floating-point type.
    • If neither condition is met, the template instantiation fails, and the function is not generated (this is an example of SFINAE).

    Example: SFINAE with Type Traits

    #include <type_traits>
    #include <iostream>
    
    template <typename T>
    void foo(T t, typename std::enable_if<std::is_integral<T>::value>::type* = 0) {
        std::cout << "foo called with integral type" << std::endl;
    }
    
    template <typename T>
    void foo(T t, typename std::enable_if<std::is_floating_point<T>::value>::type* = 0) {
        std::cout << "foo called with floating-point type" << std::endl;
    }
    
    int main() {
        foo(10);       // Calls integral version
        foo(10.5);     // Calls floating-point version
        // foo("hello"); // Error, neither integral nor floating-point
    }

    In this example:

    • The foo function template uses std::enable_if in the function signature to control which function version is selected based on the type of T.
    • The correct version is selected based on whether T is an integral or floating-point type.

    Transformation Type Traits

    Besides querying properties of types, C++ type traits can transform types at compile-time. Examples include:

    • std::remove_const<T>: Removes const qualifier from a type.
    • std::add_pointer<T>: Adds a pointer to a type.
    • std::remove_reference<T>: Removes reference from a type.
    • std::decay<T>: Converts array or function types to pointers, and removes references and cv-qualifiers.

    Example: std::remove_const

    #include <type_traits>
    #include <iostream>
    
    int main() {
        typedef const int ConstInt;
        typedef std::remove_const<ConstInt>::type NonConstInt;
        
        std::cout << std::is_const<ConstInt>::value << std::endl;  // true
        std::cout << std::is_const<NonConstInt>::value << std::endl;  // false
    }

    In this example:

    • std::remove_const removes the const qualifier from ConstInt, and NonConstInt is not a const type.

    List of Common Type Traits

    Here’s a list of some common type traits from the C++ standard library:

    • Type Property Queries:
      • std::is_arithmetic: Checks if a type is either integral or floating-point.
      • std::is_class: Checks if a type is a class.
      • std::is_enum: Checks if a type is an enumeration.
      • std::is_trivially_copyable: Checks if a type can be trivially copied.
    • Type Modifiers/Transformers:
      • std::remove_reference, std::add_reference: Removes/adds a reference to a type.
      • std::remove_pointer, std::add_pointer: Removes/adds a pointer to a type.
      • std::make_signed, std::make_unsigned: Converts a type to its signed/unsigned counterpart.
    • Type Relationships:
      • std::is_base_of<Base, Derived>: Checks if one type is derived from another.
      • std::is_convertible<From, To>: Checks if one type is convertible to another.

    Conclusion

    Type traits provide powerful tools for querying and transforming types at compile time in C++. They are a critical part of template metaprogramming and allow programmers to make compile-time decisions based on type properties. Whether used for type checking, enabling/disabling template instantiations, or optimizing code, type traits allow for more robust, flexible, and efficient C++ code.