C++ Concepts and Constraints | Type Requirements in C++20
이 글의 핵심
C++20 concepts and constraints: requires clauses, standard concepts, custom concepts, and how they replace verbose SFINAE for clearer template errors. Full examples with requires expressions, standard library concepts, and constrained overloads.
The Problem with Unconstrained Templates
Templates in C++ can accept any type — which means bad type errors appear at instantiation, buried deep in the implementation:
template<typename T>
T sum(const std::vector<T>& v) {
T result{};
for (const auto& x : v) result += x; // error here if T has no +=
return result;
}
// Calling with a type that doesn't support +=
std::vector<std::string> words = {"hello", "world"};
sum(words);
// Error: no match for 'operator+=' for 'std::string'
// ... in instantiation of 'T sum<T>(const std::vector<T>&) [with T=std::string]'
// ... (many more lines)
With a concept, the error appears at the call site with a clear message:
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
{ a += b };
};
template<Addable T>
T sum(const std::vector<T>& v) { /* ... */ }
sum(words);
// Error: constraints not satisfied for 'sum' — 'std::string' does not satisfy 'Addable'
Defining Concepts
A concept is a compile-time predicate on template parameters:
#include <concepts>
// Simple type trait concept
template<typename T>
concept Integral = std::is_integral_v<T>;
// Requires expression — checks that specific operations compile
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // expression must be valid
a += b; // this too
};
// With return type constraint
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>; // must return something convertible to bool
{ a == b } -> std::same_as<bool>; // must return exactly bool
};
// Compound concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// Nested requirements
template<typename T>
concept Sortable = requires(T container) {
typename T::value_type; // type must exist
{ container.begin() } -> std::forward_iterator;
{ container.end() } -> std::forward_iterator;
requires std::totally_ordered<typename T::value_type>; // nested requires
};
Standard Library Concepts
<concepts> provides a rich set of ready-made concepts:
#include <concepts>
// Type categories
std::integral<T> // int, long, char, bool, ...
std::floating_point<T> // float, double, long double
std::signed_integral<T> // signed integer types
std::unsigned_integral<T> // unsigned integer types
std::arithmetic<T> // integral or floating_point
// Relationships
std::same_as<T, U> // T and U are the same type
std::derived_from<T, Base> // T is derived from Base
std::convertible_to<T, U> // T is implicitly convertible to U
std::common_with<T, U> // T and U share a common type
// Comparison
std::equality_comparable<T> // T has == and !=
std::totally_ordered<T> // T has <, <=, >, >=
std::three_way_comparable<T> // T has <=>
// Callable
std::invocable<F, Args...> // F can be called with Args
std::regular_invocable<F, Args...> // same + equality preserving
std::predicate<F, Args...> // F returns bool-like
// Object concepts
std::copyable<T> // copy constructible and assignable
std::movable<T> // move constructible and assignable
std::regular<T> // copyable, equality comparable
Four Ways to Apply a Concept
template<typename T>
concept Printable = requires(std::ostream& os, T t) {
{ os << t } -> std::same_as<std::ostream&>;
};
// 1. Abbreviated function template (simplest)
void print(const Printable auto& value) {
std::cout << value << '\n';
}
// 2. Requires clause after template parameters
template<typename T>
requires Printable<T>
void print(const T& value) {
std::cout << value << '\n';
}
// 3. Constrained template parameter (classic concept syntax)
template<Printable T>
void print(const T& value) {
std::cout << value << '\n';
}
// 4. Inline requires expression (for one-off constraints)
template<typename T>
requires requires(std::ostream& os, T t) { os << t; }
void print(const T& value) {
std::cout << value << '\n';
}
All four are equivalent. Style 1 (abbreviated) is the most concise for simple cases; style 2 (requires clause) handles complex multi-concept constraints most readably.
Requires Expressions in Detail
A requires expression tests whether operations on template parameters are valid at compile time:
template<typename T>
concept Container = requires(T c, const T cc) {
// Type requirements — nested type must exist
typename T::value_type;
typename T::iterator;
// Simple expression — must compile (return type not checked)
c.clear();
// Compound expression — {expr} -> type constraint
{ c.size() } -> std::convertible_to<std::size_t>;
{ c.begin() } -> std::same_as<typename T::iterator>;
{ cc.begin() } -> std::same_as<typename T::const_iterator>;
{ c.empty() } -> std::convertible_to<bool>;
// Nested requirement — must evaluate to true
requires std::copyable<typename T::value_type>;
};
// Test it
static_assert(Container<std::vector<int>>); // passes
static_assert(Container<std::list<double>>); // passes
// static_assert(Container<int>); // fails — int has no .size() etc.
Custom Concepts: Real Examples
Hashable Type
template<typename T>
concept Hashable = requires(T t) {
{ std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};
template<Hashable K, typename V>
class HashMap {
std::unordered_map<K, V> data_;
public:
void insert(const K& key, const V& value) { data_[key] = value; }
std::optional<V> get(const K& key) const {
auto it = data_.find(key);
if (it == data_.end()) return std::nullopt;
return it->second;
}
};
HashMap<std::string, int> wordCount; // OK — string is Hashable
// HashMap<std::vector<int>, int> map; // error — vector is not Hashable
Serializable Type
template<typename T>
concept Serializable = requires(T t, std::ostream& os, std::istream& is) {
{ t.serialize(os) } -> std::same_as<void>;
{ T::deserialize(is) } -> std::same_as<T>;
};
template<Serializable T>
void saveToFile(const T& obj, const std::string& path) {
std::ofstream f(path);
obj.serialize(f);
}
Iterator Concepts
template<typename It>
concept InputIter = requires(It it) {
*it; // dereferenceable
++it; // pre-increment
it != it; // comparable
};
template<InputIter It>
auto accumulate(It first, It last) {
using T = std::remove_cvref_t<decltype(*first)>;
T sum{};
for (; first != last; ++first) sum += *first;
return sum;
}
Concepts for Overload Resolution
Concepts can select between overloads — more specific constraints win:
#include <concepts>
#include <iostream>
// Generic fallback
template<typename T>
void process(T val) {
std::cout << "Generic: " << val << '\n';
}
// More constrained — selected when T is integral
template<std::integral T>
void process(T val) {
std::cout << "Integer: " << val << " (bits: " << sizeof(T)*8 << ")\n";
}
// Even more constrained — selected when T is exactly int
void process(int val) {
std::cout << "Exact int: " << val << '\n';
}
int main() {
process(3.14); // Generic: 3.14
process(42L); // Integer: 42 (bits: 64)
process(42); // Exact int: 42
process("hello"); // Generic: hello
}
Concepts vs SFINAE
The same constraint, old and new style:
// SFINAE (pre-C++20) — cryptic, easy to get wrong
template<typename T,
std::enable_if_t<std::is_integral_v<T> &&
std::is_signed_v<T>, int> = 0>
T negate(T value) { return -value; }
// Concepts (C++20) — readable, compile errors at call site
template<std::signed_integral T>
T negate(T value) { return -value; }
// Abbreviated — even shorter
auto negate(std::signed_integral auto value) { return -value; }
Error message comparison for a bad call like negate(3.14f):
- SFINAE: ~30 lines about template substitution failure, enable_if internals
- Concepts: 2 lines — “float does not satisfy signed_integral”
Compiler Support
# GCC 10+
g++ -std=c++20 main.cpp
# Clang 10+
clang++ -std=c++20 main.cpp
# MSVC 2019 16.8+
cl /std:c++20 main.cpp
Key Takeaways
- Concepts are compile-time predicates on template parameters — they constrain which types a template accepts
- Use
requiresexpressions to specify that certain operations onTmust compile, with optional return type constraints - Standard
<concepts>header providesstd::integral,std::copyable,std::invocable, and many more - The four application styles (abbreviated, requires clause, constrained parameter, inline requires) are equivalent — choose for readability
- Concepts improve overload resolution — the most constrained overload wins
- No runtime cost — all checks are compile-time
- Error messages are dramatically shorter and appear at the call site, not deep in template instantiation
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++20 concepts and constraints: requires clauses, standard concepts, custom concepts, and how they replace verbose SFINA… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++20 Modules](/en/blog/cpp-series-24-1-modules-basics/
- [C++ Copy Initialization: The = Form, explicit, and Copy](/en/blog/cpp-copy-initialization/
- [C++ map vs unordered_map: When to Use Each and How They Work](/en/blog/cpp-stl-map-unordered-map/
이 글에서 다루는 키워드 (관련 검색어)
C++, concepts, constraints, requires, C++20 등으로 검색하시면 이 글이 도움이 됩니다.