Skip to content
C Codeloom
C++

C++ Templates and Concepts

Templates are how C++ does generic code. Concepts (C++20) make that code readable and debuggable. Here is how both work together in practice.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How templates are instantiated
  • What concepts add over plain templates
  • Writing readable constraints
  • SFINAE and what concepts replace
  • Practical concept design patterns

Prerequisites

  • Basic familiarity with the language

Templates are the reason C++ can offer zero-overhead generic code. They are also the reason error messages used to span twenty terminal screens. C++20 added concepts, which turn template error messages from cryptic to actionable and let you express interface requirements directly in the language. Together, they are how modern generic C++ is written.

How a template is instantiated

A template is a code generator. When you call max(3, 7), the compiler looks at the definition, substitutes int for T, and generates a fresh function. No runtime cost, but every distinct T produces a new copy in the binary.

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

int x = max(3, 7);          // generates max<int>
double y = max(1.5, 2.5);   // generates max<double>

If T does not support operator>, the error happens deep inside the template, not at the call site. That is the historical pain point.

The mental model

Templates work by substitution. Concepts add a check before substitution.

Without concepts:
call site --> substitute T --> compile body --> ERROR deep in template

With concepts:
call site --> check concept --> if OK, substitute and compile
                     |
                     v
                CLEAR ERROR at the call site
Template instantiation with and without concepts

The compiler can refuse to instantiate a template if the type does not satisfy a concept, and it can say so with a message that points to the failed requirement.

What concepts look like

A concept is a named compile-time predicate over types.

#include <concepts>

template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template <Numeric T>
T square(T x) { return x * x; }

square(3);     // OK
square(3.14);  // OK
// square("hi"); // compile error: "hi" does not satisfy Numeric

You can also write concepts with requires expressions, which sketch the operations a type must support.

template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

This concept is satisfied by any type where a + b is well-formed and the result is convertible to T. The compiler does not need a manual list of supported types.

Constraining functions

Three equivalent syntaxes:

template <typename T>
    requires Numeric<T>
T square(T x) { return x * x; }

template <Numeric T>
T square(T x) { return x * x; }

auto square(Numeric auto x) { return x * x; }

The last form, abbreviated function templates, is the shortest. Pick the form that reads best to your team and stick with it.

Overload selection

When multiple overloads match, the most constrained wins. This replaces a lot of SFINAE machinery.

template <typename T>
void process(T x) { /* generic */ }

template <std::integral T>
void process(T x) { /* integers */ }

template <std::floating_point T>
void process(T x) { /* floats */ }

process(3);    // integer overload
process(2.5);  // float overload
process("hi"); // generic overload

In the old world, this required std::enable_if and partial ordering tricks. With concepts, the compiler picks the most specialized one automatically.

A hands-on example

A print function that adapts to its argument.

#include <concepts>
#include <iostream>
#include <ranges>
#include <string>

template <typename T>
concept Printable = requires(std::ostream& os, T x) {
    { os << x } -> std::same_as<std::ostream&>;
};

void print(Printable auto x) {
    std::cout << x << '\n';
}

void print(std::ranges::range auto&& r) {
    for (const auto& item : r) std::cout << item << ' ';
    std::cout << '\n';
}

int main() {
    print(42);
    print(std::string{"hello"});
    print(std::vector{1, 2, 3});
}

Two overloads, both constrained, both readable. The compiler picks the right one based on the argument.

SFINAE and what concepts replace

Before concepts, template overload selection relied on Substitution Failure Is Not An Error. You wrote conditional types using std::enable_if, returning unusable expressions in cases you wanted to exclude. It worked but the errors were brutal.

// old style
template <typename T,
          std::enable_if_t<std::is_integral_v<T>, int> = 0>
T half(T x) { return x / 2; }

The concept version:

template <std::integral T>
T half(T x) { return x / 2; }

Same result, much clearer.

Common pitfalls

Writing concepts that are too narrow. Container concepts that demand specific member types can exclude valid types that happen to spell things differently. Lean on the standard library concepts (std::input_iterator, std::ranges::range, etc.) before writing your own.

Forgetting that concepts are compile-time. A concept cannot dispatch on a runtime value. If you need runtime polymorphism, use virtual functions or std::variant.

Over-constraining. If a function would work for any type with operator<, do not require Numeric. Concepts should describe what the function actually needs.

Practical tips

Compose concepts. Build small, named predicates and combine them with && and ||. The combined names document the requirements.

Constrain template parameters in public APIs even if it is more verbose. The error messages your users see will be the difference between a quick fix and a debugging session.

For library code, write concept-driven overloads instead of conditionals. The intent is clearer and the compiler does more work for you.

Wrap-up

Templates remain the foundation of generic C++; concepts are the layer that finally makes them ergonomic. With concepts, your interface tells the compiler and the reader exactly what a type must do, error messages point at the right line, and SFINAE is a footnote. Reach for standard concepts first, write small custom ones when you need them, and let constrained overloads replace the old tricks.