The Ultimate Guide to C++20 Concepts | A New Era of Template Constraints

The Ultimate Guide to C++20 Concepts | A New Era of Template Constraints

이 글의 핵심

C++20 concepts: requires clauses, std::ranges predicates, and readable template errors versus SFINAE tricks.

What Are C++20 Concepts and Why Do We Need Them?

Problem Scenario: The Template Error Message Nightmare

The Problem: Passing an incorrect type to a template function often results in error messages spanning hundreds of lines.

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    add("hello", "world");  
    // Error: no operator+ for const char*
    // 50 lines of template instantiation error messages...
}

The Solution: Concepts allow you to specify constraints on template arguments, producing clear and immediate errors when an invalid type is used.

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 concise error message!
}
flowchart TD
    subgraph before["Before C++20"]
        call1["add(string, string)"]
        inst1["Template Instantiation"]
        err1["50-line Error Message"]
    end
    subgraph after["With C++20 Concepts"]
        call2["add(string, string)"]
        check["Concept Check"]
        err2["Clear Error: Addable Violation"]
    end
    call1 --> inst1 --> err1
    call2 --> check --> err2

Table of Contents

  1. Basic Syntax: concept, requires
  2. Standard Concepts
  3. Writing Custom Concepts
  4. requires Expressions
  5. Concept Composition
  6. Common Errors and Solutions
  7. Production Patterns
  8. Complete Example: Generic Container
  9. SFINAE vs Concepts
  10. Migration Guide

1. Basic Syntax: concept, requires

Defining a Concept

#include <concepts>

// Basic form
template<typename T>
concept MyConstraint = /* boolean expression */;

// Example: Types that support addition
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

Using a Concept

// Method 1: template<Concept T>
template<Addable T>
T add(T a, T b) {
    return a + b;
}

// Method 2: requires clause
template<typename T>
    requires Addable<T>
T add(T a, T b) {
    return a + b;
}

// Method 3: trailing requires
template<typename T>
T add(T a, T b) requires Addable<T> {
    return a + b;
}

// Method 4: auto (abbreviated function template)
auto add(Addable auto a, Addable auto b) {
    return a + b;
}

2. Standard Concepts

Type Categories

#include <concepts>

// Integral 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 types
template<typename From, typename To>
    requires std::convertible_to<From, To>
To convert(From value) {
    return static_cast<To>(value);
}

// Derived 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;
}

Callable Concepts

// Callable
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. Writing 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();  // size() member function exists
};

Type Requirements

template<typename T>
concept HasValueType = requires {
    typename T::value_type;  // value_type type member 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 requirements
    requires std::default_initializable<T>;
    requires sizeof(T) <= 64;
};

(Translation continues in the same style for the remaining sections…)

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++20 Modules 완벽 가이드 | 헤더 파일을 넘어서
  • C++20 Coroutines 완벽 가이드 | 비동기 프로그래밍의 새 시대
  • C++ SFINAE | “Substitution Failure Is Not An Error” 가이드
  • C++ enable_if | “조건부 컴파일” 가이드


이 글에서 다루는 키워드 (관련 검색어)

C++, concept, cpp20, template, constraint, requires 등으로 검색하시면 이 글이 도움이 됩니다.