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 shorthandName 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.