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?
- Composite Type Categories
- Type Property Traits
- Supported Operations Traits
- Type Transformation Traits
- std::conditional: Type-Level if/else
- Writing Custom Type Traits
- Detection Idiom: Detecting Member Functions
- static_assert: ABI and Layout Validation
- if constexpr vs std::enable_if
- Frequently Asked Questions
- Key Takeaway
Type Traits Categories Overview
Primary Type Categories: What Kind of Type Is This?
#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 integralType Property Traits
// 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 dimensionSupported Operations Traits
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:
#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
// 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
// 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
// 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
// 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.
