CppConcepts (C++20)

Concepts (C++20)

Templates are powerful, but they have a long-standing usability problem: when you use a template with a type it wasn't designed for, the resulting compiler error can be enormous, cryptic, and point deep inside library internals instead of at your actual mistake. Concepts, introduced in C++20, let you write constraints directly into a template's signature — describing what a type must support to be used with it — so the compiler can reject bad calls immediately with a clear, direct error message.
The problem: unreadable template errors
Consider a simple generic add function with no constraints on T. If someone calls it with a type that doesn't support +, the error can be genuinely painful to read.

unconstrained.cpp

CPP
template <typename T>
T add(T a, T b) {
    return a + b;
}

struct NotAddable {};

int main() {
    NotAddable x, y;
    add(x, y); // error!
}
A real (abbreviated) template error message
Pre-concepts compilers typically produce something like this — long, indirect, and focused on the failed expression deep inside the template body rather than the actual problem at the call site:
error: no match for 'operator+' (operand types are 'NotAddable' and 'NotAddable')
   in instantiation of function template specialization 'add<NotAddable>' requested here
    add(x, y);
    ^
note: candidate template ignored: requirement 'is_same<...>::value' was not satisfied
...(often followed by dozens more lines of template backtrace)...
Defining and using a concept
A concept is a named, reusable compile-time predicate over types. You define one with the concept keyword and a requires expression describing what must be valid for a type to satisfy it, then apply it to a template with requires in the template signature.

numeric_concept.cpp

CPP
#include <iostream>
#include <type_traits>

// A concept that only accepts arithmetic types (int, double, etc.).
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

// Constrain the template with "requires Numeric<T>".
template <typename T>
requires Numeric<T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 4) << std::endl;     // OK, int is Numeric
    std::cout << add(1.5, 2.5) << std::endl; // OK, double is Numeric

    // add(std::string("a"), std::string("b")); // fails to compile,
    //   with a clear message: "constraints not satisfied"

    return 0;
}
There's also a shorter syntax that puts the concept name directly in place of typename, which reads very naturally for simple cases:

numeric_concept_shorthand.cpp

CPP
#include <type_traits>

template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

// Equivalent to the "requires Numeric<T>" version above.
Numeric auto add(Numeric auto a, Numeric auto b) {
    return a + b;
}
The payoff: clear error messages
When a call violates a concept, C++20 compilers report it directly and concisely — something close to: “constraints not satisfied: NotAddable does not satisfy Numeric” — pointing straight at the actual problem instead of unwinding through the entire template instantiation chain. This alone is one of the most practically valuable additions in C++20 for anyone who writes or uses generic code.
Standard library concepts
The standard library ships a set of ready-made concepts in the <concepts> header so you don't have to reinvent common ones yourself, including std::integral (integer types), std::floating_point, std::same_as, std::copyable, and std::equality_comparable.

std_concepts.cpp

CPP
#include <concepts>
#include <iostream>

template <std::integral T>
T square(T x) {
    return x * x;
}

int main() {
    std::cout << square(5) << std::endl;   // OK, int is integral
    // std::cout << square(5.0) << std::endl; // error: double is not integral
    return 0;
}
Concepts don't change what code can run, only when errors surface
A concept doesn't add new runtime capability — it's purely a compile-time constraint that documents intent and produces better diagnostics. Code that would eventually fail to compile anyway (due to a missing operator or member) still fails; concepts just make where and why it fails obvious instead of buried in a wall of template backtrace.
  • concept Name = <compile-time boolean expression>; defines a reusable named constraint.

  • template <typename T> requires Name<T> (or the shorthand Name auto param) constrains a template to types satisfying it.

  • Violating a concept produces a direct, readable compiler error instead of a deep template instantiation backtrace.

  • <concepts> provides ready-made ones: std::integral, std::floating_point, std::same_as, and more.