Concepts and Type Traits

Concepts and Type Traits

Write clearer generic code with explicit constraints and standard type queries.

Concepts and Type Traits

Why concepts improve templates

Traditional templates often fail with long error messages far from the call site. Concepts let you declare what a type must support.

template <typename T>
concept Printable = requires(T value) {
    std::cout << value;
};

template <Printable T>
void print_line(const T& value) {
    std::cout << value << '\n';
}

This is easier to read than layers of enable_if.

It also fails closer to the call site. That matters because template diagnostics are only useful if readers can connect the failure to the actual API contract.

Composing constraints

template <typename T>
concept SmallIntegral = std::integral<T> && (sizeof(T) <= 4);

Compose constraints when the combined name communicates intent better than raw expressions.

template <typename T>
concept SortableValue = std::totally_ordered<T> && std::movable<T>;

Good concept names read like design vocabulary, not like implementation trivia.

A more practical concept

template <typename T>
concept ReservableRange = requires(T container, std::size_t n) {
    container.reserve(n);
    container.begin();
    container.end();
};

template <ReservableRange T>
void preallocate_like(T& container, std::size_t count) {
    container.reserve(count);
}

This kind of constraint documents API expectations directly instead of burying them in template instantiation failures.

Larger worked example

template <typename T>
concept Validatable = requires(const T& value) {
    { value.validate() } -> std::same_as<bool>;
};

template <Validatable T>
bool all_valid(const std::vector<T>& values) {
    for (const auto& value : values) {
        if (!value.validate()) {
            return false;
        }
    }
    return true;
}

This is what concepts are for in real code: expressing the contract once so the algorithm can stay small and obvious.

Concepts versus type traits

Use concepts at the interface boundary and traits inside generic implementation details.

Traits inside implementation

template <typename T>
void log_kind(const T& value) {
    if constexpr (std::is_floating_point_v<T>) {
        std::cout << "floating-point\n";
    } else if constexpr (std::is_integral_v<T>) {
        std::cout << "integral\n";
    } else {
        std::cout << "other\n";
    }
}

The public API does not need a concept here because the function is willing to accept many kinds of types. Traits are the right tool for the internal branching.

Useful type traits

template <typename T>
void describe_value(T&& value) {
    using Raw = std::remove_cvref_t<T>;
    if constexpr (std::is_integral_v<Raw>) {
        std::cout << "integral\n";
    }
}

if constexpr

template <typename T>
void describe(const T& value) {
    if constexpr (std::integral<T>) {
        std::cout << "integral\n";
    } else {
        std::cout << "non-integral\n";
    }
}

Use it when one generic implementation needs a few type-specific branches.

Prefer this over tag dispatch when the branching logic is small and local.

Traits and transformations together

template <typename T>
using value_type_t = typename std::remove_cvref_t<T>::value_type;

Traits are often most useful when they clean up dependent names or normalize incoming template arguments.

Debugging failed constraints

template <typename T>
concept StreamInsertable = requires(std::ostream& os, T value) {
    os << value;
};
template <StreamInsertable T>
void print_debug(const T& value) {
    std::cout << value << '\n';
}

If print_debug(widget) fails, start by checking whether widget actually supports operator<<. A named concept makes that diagnosis much faster than a deep template trace with no clear boundary.

Practical advice

Practical rule of thumb

Exercises

Example in practice

template <typename T>
T twice(T value) {
    return value + value;
}

int main() {
    return twice(21);
}

Try this variation

Add a concept or `static_assert` so the template fails early for the wrong type. That is usually the first quality jump in generic code.

#include <concepts>

template <std::integral T>
T twice(T value) {
    return value + value;
}