Concepts: Compile-Time Safety With Meaning
April 23, 2026 · Luciano Muratore
Concepts: Compile-Time Safety With Meaning
C++ templates are powerful, but they have a well-known problem: when something goes wrong, the error messages are notoriously cryptic. Concepts, introduced in C++20, address this directly. They let you express constraints on template parameters—and when those constraints are violated, the compiler tells you exactly why, at compile time, before any wrong behavior gets a chance to run.
To appreciate this, it helps to build an example in three stages: code that compiles silently, runs incorrectly, and then fails clearly once a concept is added.
The Setup: A Type That Looks Numeric But Isn’t
#include <concepts>
#include <iostream>
struct FakeNumber {
int value;
FakeNumber(int v) : value(v) {}
FakeNumber operator+(const FakeNumber& other) const { return FakeNumber(value + other.value); }
FakeNumber operator/(int) const { return FakeNumber(value * 2); }
};
FakeNumber has operator+ and operator/, so it looks like a numeric type. But the division operator is broken—instead of dividing, it multiplies by 2. A type that quacks like a number, but does not behave like one.
Without a Concept: Silent Wrong Behavior
Without any constraint, a generic average function accepts FakeNumber without complaint:
template <typename T>
T average(T a, T b) {
return (a + b) / static_cast<T>(2);
}
int main() {
FakeNumber x{10}, y{20};
auto result = average(x, y);
std::cout << result.value << '\n';
}
This compiles. It runs. And it produces a wrong answer silently. The division operator multiplies instead of divides, so the result is meaningless. The type was never actually numeric, but the compiler had no way to know that, and no way to say so.
This is the danger of unconstrained templates: the contract is implicit, invisible, and impossible to enforce.
With a Concept: A Clean Compile-Time Error
Now, a concept is added:
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <Number T>
T average(T a, T b) {
return (a + b) / static_cast<T>(2);
}
The Number concept constrains T to be either an integral type or a floating-point type. FakeNumber is neither. When the compiler tries to instantiate average(x, y) with T = FakeNumber, it checks the concept—and fails:
error: no matching function for call to 'average(FakeNumber&, FakeNumber&)'
note: candidate: 'template<class T> requires Number<T> T average(T, T)'
note: constraints not satisfied
note: required for the satisfaction of 'Number<T>' [with T = FakeNumber]
note: no operand of the disjunction is satisfied
The error is precise. It tells you which function was attempted, which concept was checked, which type failed, and which condition, std::integral<T> || std::floating_point<T>, was not satisfied. There is no ambiguity about what went wrong.
What Concepts Actually Do
A concept is a compile-time predicate on a type. It evaluates to true or false at instantiation time, and a false result blocks the instantiation with a clear diagnostic.
The key insight is that concepts encode intent. Without them, a template says: “I accept anything.” With them, it says: “I accept anything that satisfies these requirements.” The constraint becomes part of the function’s interface, visible to both the compiler and the reader.
This has two benefits. First, wrong usage is caught early, before the code runs, before a wrong answer propagates. Second, the error message reflects the actual contract, not a chain of substitution failures buried deep in template instantiation.
Summary
- Without a concept,
averageacceptsFakeNumbersilently, runs with broken logic, and produces a wrong result with no warning. - With the
Numberconcept, the same call is rejected at compile time with a precise, readable error. - A concept is a compile-time predicate:
concept Number = std::integral<T> || std::floating_point<T>. - Concepts make the constraints on a template explicit, enforceable, and self-documenting.
- Final Insight: The value of a concept is not just that it rejects bad types, it is that it rejects them with meaning. A good error message is not a consolation prize. It is the point.