Skip to Content
Course content

122: Type Traits and Compile-Time Introspection

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're working as a customs officer at an international airport. You don't know exactly who is going to walk through your gate next—it could be a tourist, a diplomat, or a cargo pilot. However, you have a set of rules: if they have a diplomatic passport, they go through the fast track; if they're carrying hazardous materials, they go to secondary screening; otherwise, they follow the standard line. You aren't changing who the people are; you're just inspecting their "traits" to decide how to handle them.

In C++, type traits do exactly this, but for your types. Instead of checking a passport at runtime, the compiler checks the "passport" of a template type at compile time. This allows you to write a single piece of generic code that behaves differently depending on whether a type is an integer, a pointer, a class, or something that can be copied trivially.

Asking the Compiler for the Truth

Most of this magic lives in the <type_traits> header. The core of the library is a series of templates that inherit from std::integral_constant. When you use something like std::is_floating_point<T>::value, you're essentially asking the compiler, "Does type T meet the criteria for being a float or a double?"

I've found that the most modern way to do this is using the _v suffixes (like std::is_integral_v<T>), which are just shorthand for accessing the ::value member. It cleans up the code significantly.

#include <iostream>
#include <type_traits>

template <typename T>
void print_type_info() {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "This is a whole number!" << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "This is a decimal number!" << std::endl;
    } else {
        std::cout << "This is something else entirely." << std::endl;
    }
}

Notice I used if constexpr here. This is crucial. A regular if statement is evaluated while the program is running. if constexpr is evaluated by the compiler. If the condition is false, the compiler literally throws away the branch that doesn't apply. This means you can write code in one branch that wouldn't even compile for other types, and it won't cause an error because the compiler ignores it.

Swapping Types on the Fly

Sometimes you don't just want to branch your logic; you want to change the actual type you're using based on a trait. This is where std::conditional comes in. Think of it as a ternary operator for types.

Suppose you're writing a buffer. If the data type is small (like a char), you might want to store it in a uint8_t. But if it's larger, you want a uint64_t to avoid overflow during intermediate calculations. You can't do that with a standard if because types must be fixed at compile time.

template <typename T>
struct SmartBuffer {
    // If T is a small integral type, use uint8_t, otherwise use uint64_t
    using StorageType = std::conditional_t<std::is_integral_v<T> && sizeof(T) <= 1, 
                                           uint8_t, 
                                           uint64_t>;
    StorageType data;
};

The Performance Win: Triviality

One of my favorite use cases for introspection is std::is_trivially_copyable. In the old days of C, we just used memcpy for everything. In C++, that's dangerous because if a class has a custom copy constructor or a virtual table, memcpy will corrupt the object.

By using type traits, you can write a high-performance serialization function that uses memcpy for "simple" types but falls back to a slower, safer loop for complex objects. You get the safety of C++ with the raw speed of C, and the compiler chooses the fastest path for you.




📋 Practical Task

Implementation: High-Performance Type-Aware Data Copier

Your task is to create a utility called FastCopy. This utility should take two buffers (a source and a destination) and a type T.

Implement the following logic using <type_traits> and if constexpr:

  • If T is trivially copyable, the function should use std::memcpy to move the data for maximum performance.
  • If T is not trivially copyable (meaning it has a custom copy constructor or complex structure), the function should use a for loop to copy elements one by one using the assignment operator.
  • Add a static assertion using std::is_pointer_v to ensure that the function cannot be called if T is a raw pointer, as that would lead to shallow copy bugs in this specific architecture.

Test your implementation with a simple int (trivially copyable) and a std::string or a custom class with a destructor (not trivially copyable).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.