C++20 Concepts Complete Guide | A New Era of Template Constraints

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

  1. Basic syntax: concept, requires
  2. Standard concepts
  3. Authoring custom concepts
  4. Requires expressions
  5. Combining concepts
  6. Common problems and fixes
  7. Production patterns
  8. Complete example: generic algorithm
  9. SFINAE vs concepts
  10. 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:

AspectSFINAEConcepts
ReadabilityLowerHigher
Error messagesVerboseClearer
Compile timeOften slowerOften faster
OverloadingVerboseSimpler

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

IdeaDescription
ConceptNamed template constraint
requiresStates constraints
Standard conceptsProvided in <concepts>
Custom conceptconcept Name = requires { ... }
OverloadingOverloads 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:

One-line summary: C++20 Concepts can clarify template constraints and improve errors. Next, you might want to read Coroutines.


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.


  • [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 |