Introduction
C++20 shipped with Concepts — a long‑awaited way to express constraints on template parameters right where they live. Concepts turn the old “SFINAE and traits soup” into clear, readable predicates that the compiler can check for you. If you’ve ever stared at a 20‑line enable_if just to say “this needs to be integral,” this feature is for you.
In short: a concept is a compile‑time boolean about a type (or set of types). You can ask for properties (“is this integral?”), for operations (“can I add these two?”), or for associated types. When the constraint fails, you get a helpful diagnostic instead of a cryptic template instantiation dump.
Why I like concepts
- Self‑documenting templates. The intent sits next to the parameter list, not buried in a trait alias
- Sharper errors. Constraint failure messages point at what didn’t match, not just where a substitution imploded
- Safer generic code. You narrow valid inputs up front, which is exactly what we want in modern C++
A minimal example
Here’s the classic “add only integral types” with the standard library concept std::integral:
#include <concepts>
// Calculate sum of two integrals, this function utlizes concepts
template<std::integral T>
T concept_integral_sum(T a, T b)
{
return a + b;
}
// Calculate sum of two integrals, this function utlizes enable_if_t
template<typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
T enable_if_integral_sum(T a, T b)
{
return a + b;
}
int main()
{
// concept_integral_sum(1.0, 1.0); note: candidate:
// ‘template<class T> requires integral<T>
// enable_if_integral_sum(1.0, 1.0); note: candidate:
// ‘template<class T, class> T enable_if_integral_sum(T,T)...
}
What happens here is simple: the template participates in overload resolution only when T models std::integral. If not, the call is ill‑formed, and the compiler tells you why, as seen with concept_integral_sum function compiler print above. Of course both of these have lot of mysterious error lines (like templates tend to have), but the concept version gets the point across on the very beginning, where as enable_if_t takes quite lot of mental debugging.
Rolling your own concept
Sometimes the standard concepts are too coarse. Define exactly what you need with a requires‑expression:
#include <concepts>
template<typename T>
concept CanConvert = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template<CanConvert T>
constexpr T can_convert_sum(T a, T b)
{
return a + b;
}
int main()
{
[[maybe_unused]] constexpr auto double_sum = can_convert_sum(1.0, 1.0);
// [[maybe_unused]] auto mixed_sum = can_convert_sum(1, 1.0); deduced conflicting types for parameter ‘T’
// (‘int’ and ‘double’)
}
The requires body is a tiny compile‑time test bench. If each expression type‑checks and the trailing requirement (convertible_to<T>) holds, the concept is satisfied. In example above the double_sum compile time constant is calculated in compile time and will result in 2.0, however the runtime mixed_sum will generate nice readable error.
Combining and shaping constraints
You can combine concepts in a requires‑clause or create your own:
// Inline requires either integral or floating point
template<typename T>
requires(std::integral<T> || std::floating_point<T>) T inline_concept_sum(T a, T b)
{
return a + b;
}
// Generate own concept
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
// Uses Arithmetic concept
template<Arithmetic T>
T named_concept_sum(T a, T b)
{
return a + b;
}
Under the hood, the rules are still just “participate only if the predicate is true”. Clean and readable.
Why this matters for static polymorphism
Concepts fit right in for static polymorphism: they let you select code paths at compile time based on meaningful requirements, while keeping overload sets tidy and intent obvious. That’s the sweet spot between raw templates and virtual dispatch, which is essential in “Zero-Cost Abstractions in Embedded C++”. However before getting more into that you can check out my libconcept -library on my examples -repo.