C++Generic Programming

C++ Type Traits & static_assert: Defensive Compile-Time Metaprogramming

TT
TopicTrick Team
C++ Type Traits & static_assert: Defensive Compile-Time Metaprogramming

C++ Type Traits & static_assert: Defensive Compile-Time Metaprogramming


Table of Contents


Type Traits Categories Overview


Primary Type Categories: What Kind of Type Is This?

cpp
#include <type_traits>

// Every type satisfies exactly ONE of these:
static_assert(std::is_void_v<void>);
static_assert(std::is_nullptr_t_v<std::nullptr_t>);
static_assert(std::is_integral_v<int>);        // int, long, char, bool, etc.
static_assert(std::is_floating_point_v<float>); // float, double, long double
static_assert(std::is_array_v<int[5]>);
static_assert(std::is_enum_v<std::byte>);
static_assert(std::is_union_v</* union type */>);
static_assert(std::is_class_v<std::string>);   // class or struct (non-union)
static_assert(std::is_function_v<int(int)>);   // function type (not pointer)
static_assert(std::is_pointer_v<int*>);
static_assert(std::is_lvalue_reference_v<int&>);
static_assert(std::is_rvalue_reference_v<int&&>);
static_assert(std::is_member_function_pointer_v<int(std::string::*)()>);

// Note: char is integral!
static_assert(std::is_integral_v<char>);   // true
static_assert(std::is_integral_v<bool>);   // true

// Note: const doesn't change the primary category:
static_assert(std::is_integral_v<const int>); // true — const int is still integral

Type Property Traits

cpp
// CV qualifiers:
static_assert(std::is_const_v<const int>);      // true
static_assert(std::is_volatile_v<volatile int>); // true
static_assert(!std::is_const_v<int>);            // false

// Class properties:
struct Empty {};
struct Final final {};
struct Abstract { virtual void f() = 0; };
struct Pod { int x; double y; };

static_assert(std::is_empty_v<Empty>);          // No data members
static_assert(std::is_final_v<Final>);          // Cannot be inherited
static_assert(std::is_abstract_v<Abstract>);    // Has pure virtual functions
static_assert(std::is_trivial_v<Pod>);          // Trivial – memcpy-able
static_assert(std::is_standard_layout_v<Pod>); // Compatible with C layout
static_assert(std::is_trivially_copyable_v<Pod>); // Safe to memcpy

// Size and alignment:
static_assert(std::alignment_of_v<double> == 8); // Platform-dependent
static_assert(std::rank_v<int[3][4]> == 2);      // Array dimensions
static_assert(std::extent_v<int[3][4]> == 3);    // Size of first dimension

Supported Operations Traits

cpp
struct Simple { Simple() = default; };
struct NoDefault { NoDefault() = delete; };
struct MoveOnly {
    MoveOnly(MoveOnly&&) = default;
    MoveOnly(const MoveOnly&) = delete;
};

// Construction:
static_assert(std::is_default_constructible_v<Simple>);   // Simple{} is valid
static_assert(!std::is_default_constructible_v<NoDefault>);
static_assert(std::is_copy_constructible_v<Simple>);
static_assert(!std::is_copy_constructible_v<MoveOnly>);
static_assert(std::is_move_constructible_v<MoveOnly>);    // MoveOnly(MoveOnly&&) ok

// nothrow variants (critical for noexcept move):
static_assert(std::is_nothrow_move_constructible_v<Simple>);
// If false → std::vector will COPY instead of move during reallocation!

// Destructibility:
static_assert(std::is_destructible_v<Simple>);
static_assert(std::is_nothrow_destructible_v<Simple>);

// Assignment:
static_assert(std::is_copy_assignable_v<Simple>);
static_assert(std::is_move_assignable_v<Simple>);
static_assert(std::is_nothrow_move_assignable_v<Simple>);

Type Transformation Traits

Transformation traits produce a new type — used in template helpers and wrappers:

cpp
#include <type_traits>
#include <string>

// Remove qualifiers:
using T1 = std::remove_const_t<const int>;         // int
using T2 = std::remove_volatile_t<volatile double>; // double
using T3 = std::remove_cv_t<const volatile int>;    // int
using T4 = std::remove_reference_t<int&>;           // int
using T5 = std::remove_reference_t<int&&>;          // int

// Add qualifiers:
using T6 = std::add_const_t<int>;       // const int
using T7 = std::add_lvalue_reference_t<int>; // int&
using T8 = std::add_pointer_t<int>;     // int*

// Combined transformation:
using T9 = std::decay_t<const int[5]>;  // int* (array→pointer, removes const)
using T10 = std::decay_t<const int&>;   // int  (removes ref, removes const)
using T11 = std::decay_t<void(int)>;    // void(*)(int) (function→pointer)

// Practical: remove ref from template arg
template<typename T>
class Box {
    // Store T by value — strip any reference the user might pass
    using StoredType = std::remove_reference_t<T>;
    StoredType data_;
public:
    Box(T&& val) : data_(std::forward<T>(val)) {}
};

// result_of / invoke_result (what type does F(Args...) return?):
auto fn = [](int x) -> double { return x * 2.0; };
using Result = std::invoke_result_t<decltype(fn), int>; // double
static_assert(std::is_same_v<Result, double>);

std::conditional: Type-Level if/else

cpp
// conditional<condition, TrueType, FalseType>
template<typename T>
using StorageType = std::conditional_t<
    (sizeof(T) <= 8),   // Condition
    T,                  // True: store by value (small types)
    std::unique_ptr<T>  // False: store on heap (large types)
>;

using SmallStorage = StorageType<int>;            // int
using LargeStorage = StorageType<std::string>;   // unique_ptr<string>
                                                  // (sizeof(string) > 8)

// Nested conditional (type-level if/else if/else):
template<typename T>
using BestContainer = std::conditional_t<
    std::is_integral_v<T>,
        std::vector<T>,          // For integers: vector
    std::conditional_t<
        std::is_floating_point_v<T>,
            std::deque<T>,       // For floats: deque
            std::list<T>         // Otherwise: list
    >
>;

Writing Custom Type Traits

cpp
// Pattern 1: Inherit from true_type / false_type
template<typename T>
struct is_smart_pointer : std::false_type {};

template<typename T>
struct is_smart_pointer<std::unique_ptr<T>> : std::true_type {};

template<typename T>
struct is_smart_pointer<std::shared_ptr<T>> : std::true_type {};

template<typename T>
constexpr bool is_smart_pointer_v = is_smart_pointer<T>::value;

static_assert(is_smart_pointer_v<std::unique_ptr<int>>); // true
static_assert(!is_smart_pointer_v<int*>);                 // false — raw ptr

// Pattern 2: Using void_t for detection (C++17)
template<typename T, typename = void>
struct has_serialize : std::false_type {};

template<typename T>
struct has_serialize<T,
    std::void_t<decltype(std::declval<T>().serialize())>
> : std::true_type {};

struct WithSerialize { void serialize(); };
struct WithoutSerialize {};

static_assert(has_serialize<WithSerialize>::value);    // true
static_assert(!has_serialize<WithoutSerialize>::value); // false

// Pattern 3: Concept-based trait (C++20 — recommended)
template<typename T>
concept Serializable = requires(T v) { v.serialize(); };

// Now use in template constraints directly!

Detection Idiom: Detecting Member Functions

cpp
// Detect if T has a .size() member function returning integral:
template<typename T, typename = void>
struct has_size : std::false_type {};

template<typename T>
struct has_size<T,
    std::void_t<
        std::enable_if_t<
            std::is_integral_v<
                decltype(std::declval<T>().size())
            >
        >
    >
> : std::true_type {};

// In C++20, replace with concept (far cleaner):
template<typename T>
concept HasSize = requires(T v) {
    { v.size() } -> std::integral;
};

// Usage with if constexpr:
template<typename T>
void safe_print_size(const T& container) {
    if constexpr (HasSize<T>) {
        std::println("Size: {}", container.size());
    } else {
        std::println("(size unknown)");
    }
}

static_assert: ABI and Layout Validation

cpp
// Platform ABI guarantees:
static_assert(sizeof(int)       == 4,  "Requires 32-bit int");
static_assert(sizeof(void*)     == 8,  "Requires 64-bit pointers");
static_assert(sizeof(long long) == 8,  "Requires 64-bit long long");
static_assert(sizeof(float)     == 4,  "Requires IEEE 754 float");
static_assert(sizeof(double)    == 8,  "Requires IEEE 754 double");

// Network packet layout must be exact:
#pragma pack(push, 1)
struct EthernetHeader {
    uint8_t  dst_mac[6];
    uint8_t  src_mac[6];
    uint16_t ethertype;
};
#pragma pack(pop)

static_assert(sizeof(EthernetHeader)   == 14, "EthernetHeader must be 14 bytes");
static_assert(offsetof(EthernetHeader, ethertype) == 12, "ethertype at byte 12");

// Template constraint validation:
template<typename T>
class AtomicCounter {
    static_assert(std::is_integral_v<T>,
        "AtomicCounter<T>: T must be an integral type");
    static_assert(sizeof(T) <= 8,
        "AtomicCounter<T>: T must fit in 8 bytes for hardware atomics");
    static_assert(!std::is_same_v<T, bool>,
        "AtomicCounter<T>: bool is not valid for a counter");
    
    std::atomic<T> value_{0};
public:
    T increment() { return ++value_; }
};

Frequently Asked Questions

When should I use type traits vs Concepts? Use Concepts in function/class template parameter lists for constraints (better error messages). Use type traits inside function bodies with if constexpr for compile-time branching (generate different code for different types). Use static_assert + type traits for ABI and layout guarantees that must be enforced unconditionally. All three work together — they're not mutually exclusive.

What is void_t and why does it work? std::void_t<...> maps any number of valid types to void. In a class template partial specialization, if any of the types in void_t<...> fail to form (because an expression is invalid), SFINAE kicks in and the specialization is discarded. This is how the detection idiom works — the decltype(...) expression fails if the member function doesn't exist, discarding the true_type specialization.

Are type traits available in constexpr context? Yes — all _v variable templates (std::is_integral_v<T>, etc.) are constexpr bool and can be used in if constexpr, static_assert, template argument positions, and any compile-time context. They're computed entirely at compile time.


Key Takeaway

Type traits are the introspection toolkit that makes C++ generic code safe and precise. They answer "what is this type?" at compile time, enabling code that branches, validates, and transforms based on type properties — entirely without runtime overhead. Combined with Concepts (which use the same type-checking machinery under the hood), if constexpr for dispatch, and static_assert for validation, type traits are the scaffolding of every professional C++ library.

Read next: Project: Building a Modular Plugin System →


Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineering.