C++20 Concepts Complete Guide | A New Era of Template Constraints
이 글의 핵심
Practical guide to C++20 concepts: syntax, standard library concepts, composition, and production patterns.
What are C++20 concepts? Why do we need them?
Problem scenario: template error message overload
Problem: If you pass the wrong type to a template function, the error message will be hundreds of lines long.
template<typename T>
T add(T a, T b) {
return a + b;
}
int main() {
add("hello", "world");
// Error: no operator+ for const char*
// Dozens of lines of instantiation errors...
}
Solution: Concepts specifies constraints in template arguments, so that if an incorrect type is entered, an immediate clear error is issued.
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template<Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
add("hello", "world");
// Error: const char* does not satisfy Addable
// Clear and short error message!
}
flowchart TD
subgraph before["Before C++17"]
call1["add(string, string)"]
inst1["Template instantiation"]
err1["Long error message"]
end
subgraph after["C++20 concepts"]
call2["add(string, string)"]
check["Concept check"]
err2["Clear error: Addable not satisfied"]
end
call1 --> inst1 --> err1
call2 --> check --> err2
Table of contents
- Basic syntax: concept, requires
- Standard concepts
- Authoring custom concepts
- Requires expressions
- Combining concepts
- Common problems and fixes
- Production patterns
- Complete example: generic algorithm
- SFINAE vs concepts
- Migration guide
1. Basic syntax: concept and requires
Defining a concept
#include <concepts>
// Basic form
template<typename T>
concept MyConstraint = /* boolean expression */;
// Example: addable type
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
Using concepts
// Option 1: template<Concept T>
template<Addable T>
T add(T a, T b) {
return a + b;
}
// Option 2: requires clause
template<typename T>
requires Addable<T>
T add(T a, T b) {
return a + b;
}
// Option 3: Trailing requires
template<typename T>
T add(T a, T b) requires Addable<T> {
return a + b;
}
// Option 4: auto (abbreviated function template)
auto add(Addable auto a, Addable auto b) {
return a + b;
}
2. Standard concepts
Type classification
#include <concepts>
// Integer types
template<std::integral T>
T square(T x) {
return x * x;
}
// Floating-point types
template<std::floating_point T>
T sqrt_approx(T x) {
return x / 2;
}
// Signed integers
template<std::signed_integral T>
T negate(T x) {
return -x;
}
// Unsigned integers
template<std::unsigned_integral T>
T increment(T x) {
return x + 1;
}
int main() {
square(5); // OK: int
sqrt_approx(9.0); // OK: double
negate(-10); // OK: int
increment(10u); // OK: unsigned int
}
Relationship concepts
// Same type
template<typename T, typename U>
requires std::same_as<T, U>
void func(T a, U b) {
// T and U are the same type
}
// Convertible
template<typename From, typename To>
requires std::convertible_to<From, To>
To convert(From value) {
return static_cast<To>(value);
}
// Derived-from relationship
template<typename Derived, typename Base>
requires std::derived_from<Derived, Base>
void process(Derived* ptr) {
Base* base = ptr; // OK
}
Comparison concepts
// Equality comparable
template<std::equality_comparable T>
bool is_equal(T a, T b) {
return a == b;
}
// Totally ordered
template<std::totally_ordered T>
T max(T a, T b) {
return (a > b) ? a : b;
}
Invocable concepts
// Invocable
template<typename F, typename... Args>
requires std::invocable<F, Args...>
auto call(F func, Args... args) {
return func(args...);
}
// Predicate (returns bool)
template<typename F, typename T>
requires std::predicate<F, T>
bool test(F pred, T value) {
return pred(value);
}
Object concepts
// Default constructible
template<std::default_initializable T>
T create() {
return T{};
}
// Copy constructible
template<std::copy_constructible T>
T duplicate(const T& value) {
return T(value);
}
// Move constructible
template<std::move_constructible T>
T transfer(T&& value) {
return T(std::move(value));
}
3. Custom concepts
Container Concept
template<typename T>
concept Container = requires(T c) {
// Type members
typename T::value_type;
typename T::iterator;
// Member functions
{ c.size() } -> std::same_as<std::size_t>;
{ c.begin() } -> std::same_as<typename T::iterator>;
{ c.end() } -> std::same_as<typename T::iterator>;
{ c.empty() } -> std::convertible_to<bool>;
};
template<Container C>
void print_size(const C& container) {
std::cout << "Size: " << container.size() << '\n';
}
int main() {
std::vector<int> v = {1, 2, 3};
print_size(v); // OK
int arr[] = {1, 2, 3};
// print_size(arr); // Error: int[] not Container
}
Serializable Concept
template<typename T>
concept Serializable = requires(T obj, std::ostream& os, std::istream& is) {
{ obj.serialize(os) } -> std::same_as<void>;
{ T::deserialize(is) } -> std::same_as<T>;
};
template<Serializable T>
void save(const T& obj, std::ostream& os) {
obj.serialize(os);
}
template<Serializable T>
T load(std::istream& is) {
return T::deserialize(is);
}
Numeric Concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
T abs(T value) {
return value < 0 ? -value : value;
}
template<Numeric T>
T clamp(T value, T min, T max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
4. Requires expressions
Simple requirements
template<typename T>
concept HasSize = requires(T t) {
t.size(); // member size() exists
};
Type requirements
template<typename T>
concept HasValueType = requires {
typename T::value_type; // nested type value_type exists
};
Compound requirements
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};
Nested requirements
template<typename T>
concept ComplexConstraint = requires(T t) {
// Simple requirement
t.method();
// Type requirement
typename T::value_type;
// Compound requirement
{ t.size() } -> std::same_as<std::size_t>;
// Nested requirement
requires std::default_initializable<T>;
requires sizeof(T) <= 64;
};
5. Combining concepts
Logical combinations
// AND
template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;
// OR
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// NOT (use requires clause for negation)
template<typename T>
requires (!std::integral<T>)
void func(T value);
Concept layering
template<typename T>
concept Movable = std::move_constructible<T> && std::movable<T>;
template<typename T>
concept Copyable = Movable<T> && std::copy_constructible<T>;
template<typename T>
concept Semiregular = Copyable<T> && std::default_initializable<T>;
template<typename T>
concept Regular = Semiregular<T> && std::equality_comparable<T>;
6. Common problems and fixes
Problem 1: Constraint violation
Symptom: error: no matching function ... constraints not satisfied.
template<std::integral T>
T square(T x) {
return x * x;
}
int main() {
// square(3.14); // Error: double does not satisfy std::integral
}
Solution: Pass the correct type or relax the Concept.
template<typename T>
requires std::integral<T> || std::floating_point<T>
T square(T x) {
return x * x;
}
int main() {
square(5); // OK
square(3.14); // OK
}
Problem 2: Circular concepts
Cause: Concept A references B and B references A.
// Circular
template<typename T>
concept ConceptA = ConceptB<T>;
template<typename T>
concept ConceptB = ConceptA<T>;
// Correct definition
template<typename T>
concept ConceptA = std::integral<T>;
template<typename T>
concept ConceptB = ConceptA<T> && std::signed_integral<T>;
Problem 3: Requires expression fails
Cause: If an expression in requires fails to compile, the concept is false.
template<typename T>
concept HasFoo = requires(T t) {
t.foo(); // false if foo() is missing
};
struct A {};
struct B { void foo(); };
static_assert(!HasFoo<A>); // OK
static_assert(HasFoo<B>); // OK
7. Production patterns
Pattern 1: Concept-based overloading
#include <concepts>
#include <iostream>
// Integral
template<std::integral T>
void print(T value) {
std::cout << "Integer: " << value << '\n';
}
// Floating-point
template<std::floating_point T>
void print(T value) {
std::cout << "Float: " << value << '\n';
}
// String
void print(const std::string& value) {
std::cout << "String: " << value << '\n';
}
int main() {
print(42); // Integer: 42
print(3.14); // Float: 3.14
print("hello"s); // String: hello
}
Pattern 2: Constraint hierarchy
// Base
template<typename T>
concept Drawable = requires(T t) {
t.draw();
};
// With color
template<typename T>
concept ColoredDrawable = Drawable<T> && requires(T t) {
t.setColor(0, 0, 0);
};
// With animation
template<typename T>
concept AnimatedDrawable = ColoredDrawable<T> && requires(T t) {
t.animate();
};
// Overloads
void render(Drawable auto& obj) {
obj.draw();
}
void render(ColoredDrawable auto& obj) {
obj.setColor(255, 0, 0);
obj.draw();
}
void render(AnimatedDrawable auto& obj) {
obj.animate();
obj.draw();
}
Pattern 3: Range concept
template<typename R>
concept Range = requires(R r) {
std::ranges::begin(r);
std::ranges::end(r);
};
template<Range R>
void process(R&& range) {
for (auto&& elem : range) {
// process
}
}
8. Complete example: generic algorithm
Sortable container
#include <concepts>
#include <vector>
#include <algorithm>
#include <iostream>
template<typename T>
concept Sortable = requires(T container) {
typename T::value_type;
{ container.begin() } -> std::same_as<typename T::iterator>;
{ container.end() } -> std::same_as<typename T::iterator>;
requires std::totally_ordered<typename T::value_type>;
};
template<Sortable C>
void sort_container(C& container) {
std::sort(container.begin(), container.end());
}
int main() {
std::vector<int> v = {3, 1, 4, 1, 5};
sort_container(v);
for (int x : v) {
std::cout << x << ' ';
}
// 1 1 3 4 5
}
9. SFINAE vs concepts
SFINAE (C++17)
#include <type_traits>
// Integral
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
T square(T x) {
return x * x;
}
// Floating-point
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T square(T x) {
return x * x;
}
Concepts (C++20)
// Integral
template<std::integral T>
T square(T x) {
return x * x;
}
// Floating-point
template<std::floating_point T>
T square(T x) {
return x * x;
}
Comparison:
| Aspect | SFINAE | Concepts |
|---|---|---|
| Readability | Lower | Higher |
| Error messages | Verbose | Clearer |
| Compile time | Often slower | Often faster |
| Overloading | Verbose | Simpler |
10. Migration guide
enable_if → concepts
Before (C++17):
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
add(T a, T b) {
return a + b;
}
After (C++20):
template<std::integral T>
T add(T a, T b) {
return a + b;
}
SFINAE traits → concepts
Before:
template<typename T, typename = void>
struct has_size : std::false_type {};
template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};
template<typename T>
std::enable_if_t<has_size<T>::value, std::size_t>
get_size(const T& container) {
return container.size();
}
After:
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
template<HasSize T>
std::size_t get_size(const T& container) {
return container.size();
}
Summary
| Idea | Description |
|---|---|
| Concept | Named template constraint |
| requires | States constraints |
| Standard concepts | Provided in <concepts> |
| Custom concept | concept Name = requires { ... } |
| Overloading | Overloads selected by concepts |
C++20 concepts clarify template errors, improve readability, and offer a modern alternative to much SFINAE.
FAQ
Q1: Concepts vs SFINAE?
A: Concepts usually win on readability, diagnostics, and often compile time. Prefer them in C++20 and later.
Q2: What is a requires expression?
A: Inside requires(T t) { ... } you list what must compile: members, nested types, operators, etc.
Q3: Can auto and Concept be used together?
A: Yes—e.g. void f(std::integral auto x) as an abbreviated function template.
Q4: Can I combine concepts?
A: Yes—combine with && / || or build layers of named concepts.
Q5: What is compiler support?
A:
- GCC 10+: full support
- Clang 10+: full support
- MSVC 2019 (16.3+): full support
Q6: What are Concepts learning resources?
A:
- cppreference - Constraints and concepts
- “C++20: The Complete Guide” by Nicolai Josuttis
- “C++ Templates: The Complete Guide” 2nd Edition
One-line summary: C++20 Concepts can clarify template constraints and improve errors. Next, you might want to read Coroutines.
Related reading (internal links)
Other posts that connect to this topic.
- Complete Guide to C++20 Modules | Beyond header files
- C++20 Coroutines Complete Guide | A new era in asynchronous programming
- C++ SFINAE | “Substitution Failure Is Not An Error” guide
- C++ enable_if | “Conditional Compilation” Guide
Practical tips
Tips you can apply at work.
Debugging
- When something breaks, check compiler warnings first
- Reproduce with a small test case
Performance
- Do not optimize without profiling
- Define measurable targets first
Code review
- Pre-check areas that often get flagged in review
- Follow team coding conventions
Production checklist
Things to verify when applying this idea in practice.
Before coding
- Is this technique the best fit for the problem?
- Can teammates understand and maintain it?
- Does it meet performance requirements?
While coding
- Are all compiler warnings addressed?
- Are edge cases considered?
- Is error handling appropriate?
At review
- Is intent clear?
- Are tests sufficient?
- Is it documented?
Use this checklist to reduce mistakes and improve quality.
Keywords covered
C++, concept, cpp20, template, constraint, requires to find this post.
Related posts
- [C++ auto type inference | Leaving complex types to the compiler
- C++ CTAD |
- C++ Concepts and Constraints |
- C++20 consteval complete guide | Compile-time only functions
- C++ constexpr if |