C++20 Concepts & Constraints: Writing Readable, Safe Generic Code
Complete C++20 Concepts guide. Understand what concepts are and how they fix cryptic template error messages, use all four constraint syntax forms (requires clause, requires expression, concept shorthand, auto concept), define custom concepts for Printable, Hashable, and Container types, combine concepts with && and ||, leverage standard library concepts from <concepts> and <ranges>, and use concepts for function overloading resolution.

C++20 Concepts & Constraints: Writing Readable, Safe Generic Code
Table of Contents
- The Problem: Template Error Wall of Text
- Four Constraint Syntax Forms
- Using Built-In Standard Library Concepts
- requires Expressions: Compound Requirements
- Defining Custom Concepts
- Combining Concepts with && and ||
- Concept-Based Overloading: Subsumption Rules
- Concepts as Template Parameter Constraints
- std::ranges Concepts for Collections
- Frequently Asked Questions
- Key Takeaway
The Problem: Template Error Wall of Text
// Without Concepts - template that expects printable types:
template<typename T>
void log(T val) {
std::cout << val << '\n'; // Requires operator<<
}
struct Widget {}; // Widget has no operator<<
log(Widget{}); // Compiles fine... but:
// ERROR: 71 lines of error messages about std::ostream, operator<<,
// function overload candidates, template instantiation history...
// NONE of which tell you "Widget doesn't support operator<<"
// WITH Concepts - single clear error:
template<typename T>
concept Printable = requires(T v, std::ostream& os) { os << v; };
template<Printable T>
void log_safe(T val) { std::cout << val << '\n'; }
log_safe(Widget{});
// ERROR: candidate template ignored: constraints not satisfied [with T = Widget]
// note: 'Widget' does not satisfy 'Printable'
// -> One line. Exact violation. Fixed in 30 seconds.Four Constraint Syntax Forms
All four forms are equivalent - choose the most readable for each context:
#include <concepts>
// Form 1: requires clause after template parameter list
template<typename T>
requires std::integral<T>
T double_it(T x) { return x * 2; }
// Form 2: requires clause after function signature
template<typename T>
T triple_it(T x) requires std::integral<T> { return x * 3; }
// Form 3: Concept shorthand (most concise for single constrained param)
template<std::integral T>
T quadruple_it(T x) { return x * 4; }
// Form 4: Abbreviated function template (C++20 - auto + concept)
auto quintuple_it(std::integral auto x) { return x * 5; }
// Exactly equivalent to Form 3
// All four are 100% equivalent - same overload resolution, same error messages
// Preference: Form 3/4 for simple cases, Form 1 for complex multi-requirement constraintsUsing Built-In Standard Library Concepts
The <concepts> header provides a comprehensive set of ready-made constraints:
#include <concepts>
// === Type category concepts ===
void process_int(std::integral auto x) { /* int/long/char/bool/... */ }
void process_fp(std::floating_point auto x) { /* float/double/long double */ }
void process_num(std::arithmetic auto x) { /* integral OR floating_point */ }
void process_signed(std::signed_integral auto x) { /* int/long/short/... */ }
// === Relationship concepts ===
template<typename From, typename To>
requires std::convertible_to<From, To>
To safe_cast(From val) { return static_cast<To>(val); }
template<typename T, typename U>
requires std::same_as<T, U> // Exactly the same type
void identical_types_only(T a, U b) {}
// === Object concepts ===
template<typename T>
requires std::copy_constructible<T> // Can be copied
void duplicate(T val) { T copy = val; }
template<typename T>
requires std::movable<T> // Can be moved
void transfer(T& src, T& dst) { dst = std::move(src); }
template<typename T>
requires std::default_initializable<T> // T{} is valid
T make_default() { return T{}; }
// === Callable concepts ===
template<typename F, typename... Args>
requires std::invocable<F, Args...> // F(Args...) is valid
auto call(F&& fn, Args&&... args) {
return std::invoke(std::forward<F>(fn), std::forward<Args>(args)...);
}
template<typename F, typename Arg>
requires std::predicate<F, Arg> // F(Arg) returns bool
void filter(std::vector<Arg>& v, F pred) {
std::erase_if(v, [&](const Arg& a){ return !pred(a); });
}requires Expressions: Compound Requirements
requires expressions check validity of expressions - they test syntax, not runtime behavior:
// Simple requirement: expression compiles
template<typename T>
concept HasSize = requires(T v) {
v.size(); // size() must be a valid call on T
};
// Type requirement: expression has a specific type
template<typename T>
concept HasValueType = requires {
typename T::value_type; // T::value_type must exist as a type
};
// Nested requirement: additional constraint on expression's result
template<typename T>
concept SizedContainer = requires(T v) {
{ v.size() } -> std::convertible_to<size_t>; // size() must return something convertible to size_t
{ v.empty() } -> std::same_as<bool>; // empty() must return exactly bool
typename T::value_type; // must have value_type
};
// Compound requirement - multiple checks:
template<typename T>
concept Serializable = requires(T v, std::ostream& out, std::istream& in) {
{ v.serialize(out) } -> std::same_as<void>; // has void serialize(ostream&)
{ T::deserialize(in) } -> std::same_as<T>; // has static T deserialize(istream&)
{ v.byte_size() } -> std::integral; // byte_size() returns integral
};Defining Custom Concepts
#include <concepts>
#include <ranges>
#include <ostream>
// Printable: can be streamed to ostream
template<typename T>
concept Printable = requires(T v, std::ostream& os) {
{ os << v } -> std::same_as<std::ostream&>;
};
// Hashable: works as key in unordered containers
template<typename T>
concept Hashable = requires(T v) {
{ std::hash<T>{}(v) } -> std::convertible_to<size_t>;
{ v == v } -> std::same_as<bool>; // Can compare for equality
};
// Numeric: arithmetic and comparable
template<typename T>
concept Numeric = std::arithmetic<T> && requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
{ a / b } -> std::same_as<T>;
{ a < b } -> std::convertible_to<bool>;
};
// Container: has begin/end/size
template<typename C>
concept Container = requires(C c) {
typename C::value_type;
typename C::iterator;
{ c.begin() } -> std::input_or_output_iterator;
{ c.end() } -> std::input_or_output_iterator;
{ c.size() } -> std::integral;
{ c.empty() } -> std::convertible_to<bool>;
};
// Usage:
template<Numeric T>
T clamp(T val, T lo, T hi) {
return std::max(lo, std::min(val, hi));
}
template<Container C>
void print_container(const C& c) {
for (const auto& v : c) std::cout << v << ' ';
std::cout << '\n';
}
template<Hashable K, typename V>
class SafeMap {
std::unordered_map<K, V> data_;
public:
V& operator[](const K& key) { return data_[key]; }
};Combining Concepts with && and ||
// Both constraints must be satisfied:
template<typename T>
requires std::integral<T> && std::signed_integral<T>
T safe_negate(T x) { return -x; }
// Either constraint is sufficient:
template<typename T>
requires std::integral<T> || std::floating_point<T>
T square(T x) { return x * x; }
// Negation:
template<typename T>
requires (!std::is_pointer_v<T>)
T safe_copy(T val) { return val; }
// Complex compound:
template<typename T>
requires (std::semiregular<T> &&
std::totally_ordered<T> &&
(sizeof(T) <= 16))
class StackSet {
// T can be default-constructed, copied, moved, compared, and fits in 2 cache words
};Concept-Based Overloading: Subsumption Rules
When multiple constrained overloads are valid, the compiler picks the most constrained one:
// General: handles any range
template<std::ranges::range R>
void sort_it(R& range) {
// Fallback: general sort using std::begin/std::end
std::vector<typename R::value_type> tmp(range.begin(), range.end());
std::sort(tmp.begin(), tmp.end());
std::ranges::copy(tmp, range.begin());
}
// More constrained: handles random-access ranges (faster algorithm)
template<std::ranges::random_access_range R>
void sort_it(R& range) {
// Better: direct in-place sort (random_access_range ⊃ range)
std::sort(range.begin(), range.end());
}
// Even more constrained: handles contiguous ranges + trivial types (SIMD-eligible)
template<std::ranges::contiguous_range R>
requires std::is_trivially_copyable_v<std::ranges::range_value_t<R>>
void sort_it(R& range) {
// Best: SIMD-optimized or radix sort
std::sort(range.begin(), range.end()); // Could be replaced with SIMD sort
}
std::vector<int> v = {5,2,8,1};
std::list<int> l = {5,2,8,1};
sort_it(v); // Uses contiguous_range overload (most constrained, vector qualifies)
sort_it(l); // Uses range overload (list is not random access)Frequently Asked Questions
Do concepts impact runtime performance? No - concepts are purely compile-time. They add zero runtime overhead. The generated machine code for a constrained template is identical to an unconstrained one; constraints only affect which instantiation is chosen and what errors are produced.
Can I use concepts to check for non-type properties, like a specific value?
No - concepts check syntactic and semantic properties of types (do they have this function? does it return this type?). They cannot check runtime values or non-type properties directly. For value-based compile-time checks, use if constexpr with static_assert, or template non-type parameters with value constraints.
Should I use concepts everywhere, or only at interfaces?
Use concepts on every public template function/class interface - they serve as documentation and provide better error messages. Inside implementation details, if constexpr + type traits are still appropriate for compile-time branching. Don't add concepts to private helper functions that aren't part of any public API.
Key Takeaway
C++20 Concepts complete the template programming story. Before: templates were "duck typing" - if it compiles, it works; if not, good luck reading the error. After: templates are precisely specified contracts. The requires clause says exactly what properties T must have, the compiler verifies this at the call site, and error messages name the violated constraint. Combined with standard library concepts (<concepts>, <ranges>), you can constrain almost any generic API without writing a single custom concept.
Read next: Meta-programming: constexpr and Compile-Time Logic ->
Part of the C++ Mastery Course - 30 modules from modern C++ basics to expert systems engineering.
