본문으로 건너뛰기 C++20 Concepts Complete Guide

C++20 Concepts Complete Guide

C++20 Concepts Complete Guide

이 글의 핵심

C++20 concepts and requires: template constraints, standard concepts, custom concepts, requires expressions, and migration from SFINAE.

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:

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

Other posts that connect to this topic.

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++, concept, cpp20, template, constraint, requires 등으로 검색하시면 이 글이 도움이 됩니다.