Přeskočit na obsah

C++20 Concepts & Constraints

Definujte přesné požadavky na argumenty šablony pomocí konceptů. Napište čistší generický kód s požadovanými klauzulemi, zkrácenými šablonami funkcí a koncepty standardních knihoven.

The Problem Concepts Solve

Before concepts, template error messages were cryptic — usingstd::sorton a type withoutoperator<could produce pages of error text deep inside the standard library implementation. Concepts solve this by letting you state requirements up front on template parameters.

A concept is a named boolean predicate evaluated at compile time. It checks whether a type satisfies a set of syntactic and semantic requirements. When a concept is not satisfied, the compiler produces a clear, concise error message pointing to the unsatisfied requirement — not a wall of template instantiation backtraces.

Concepts were introduced in C++20 and represent the most significant improvement to generic programming since templates themselves.

Definování pojmů

Koncept je definován pomocí klíčového slovaconceptnásledovaného výrazem vyžaduje výraz, který určuje, jaké operace musí typ podporovat. Výraz vyžaduje může kontrolovat platné výrazy, návratové typy, vnořené typy a další.

concept_definition.cpp
#include <concepts>
#include <iostream>
#include <string>

// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // a+b must return something convertible to T
    { a == b } -> std::convertible_to<bool>; // must be comparable
    requires std::copy_constructible<T>;     // nested requirement
};

// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
    { t.size() } -> std::convertible_to<std::size_t>;
    { t.empty() } -> std::convertible_to<bool>;
};

// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
    return a + b + c;
}

template <Sizable Container>
void report_size(const Container& c) {
    std::cout << "Size: " << c.size()
              << (c.empty() ? " (empty)" : "") << '\n';
}

int main() {
    std::cout << sum_three(1, 2, 3)       << '\n'; // 6
    std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0

    std::string s = "hello";
    report_size(s);  // Size: 5
    std::vector<int> v{1, 2, 3};
    report_size(v);  // Size: 3
}

Vyžaduje klauzule a zkrácenou syntaxi

There are multiple ways to apply a concept to a template. The requires clause goes after the template parameter list or after the function signature. The abbreviated function template syntax usesConcept autoas a parameter type, which is the most concise form.

requires_clause.cpp
#include <concepts>
#include <iostream>

// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        T temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// Method 2: requires clause after template parameters
template <typename T>
    requires std::integral<T>
T lcm(T a, T b) {
    return (a / gcd(a, b)) * b;
}

// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
    return value < 0 ? -value : value;
}

// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
    std::cout << "Integral: " << value << '\n';
}

// Concept auto works for return types too (C++20)
std::integral auto compute() {
    return 42;  // must return an integral type
}

int main() {
    std::cout << gcd(12, 8)       << '\n'; // 4
    std::cout << lcm(12, 8)       << '\n'; // 24
    std::cout << absolute(-42)    << '\n'; // 42
    print_integral(100);                    // Integral: 100
    // print_integral(3.14);  // ERROR: constraint not satisfied
}

Složení konceptu a standardní koncepty

Concepts compose naturally with && (conjunction) and || (disjunction). The standard library provides a rich set of concepts in :

Core language concepts:
-std::same_as— T and U are the same type
-std::derived_from— Derived is derived from Base
-std::convertible_to— From is implicitly convertible to To
-std::integral,std::floating_point,std::signed_integral

Comparison concepts:
-std::equality_comparable— supports==and!=
-std::totally_ordered— supports<,>,<=,>=,==,!=

Object concepts:
-std::movable,std::copyable,std::semiregular,std::regular

Callable concepts:
-std::invocable— F can be called with Args
-std::predicate— F is a predicate returning bool

Iterator concepts ():
-std::input_iterator,std::forward_iterator,std::random_access_iterator,std::contiguous_iterator

Range concepts ():
-std::ranges::range,std::ranges::sized_range,std::ranges::random_access_range

Rozlišení konceptu subsumpce a přetížení

When multiple constrained overloads match, the compiler uses concept subsumption to pick the most specific one. Concept A subsumes concept B if A's constraints logically imply B's constraints. This allows "refinement" — a more specific concept wins over a more general one.

subsumption.cpp
#include <concepts>
#include <iostream>

// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
    { os << t };
};

template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;

// Less constrained overload
template <Printable T>
void display(const T& value) {
    std::cout << "[generic] " << value << '\n';
}

// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
    std::cout << "[number] " << value << '\n';
}

int main() {
    display(42);              // [number] 42 — PrintableNumber is more specific
    display(std::string("hi")); // [generic] hi — only Printable matches
    display(3.14);            // [generic] 3.14 — not integral
}

Vyžaduje vyžaduje vzor

Poněkud neobvyklé requires requires se objeví, když použijete výraz ad-hoc vyžaduje přímo v klauzuli required bez definování pojmenovaného konceptu. Prvnírequireszavádí omezující klauzuli; druhýrequireszačíná požadovaný výraz.

requires_requires.cpp
#include <iostream>
#include <string>

// Ad-hoc constraint without naming a concept
template <typename T>
    requires requires(T a, T b) {
        { a + b } -> std::same_as<T>;
    }
T add(T a, T b) {
    return a + b;
}

// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <AddableToSelf T>
T add_named(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2)                << '\n'; // 3
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
    std::cout << add_named(10, 20)        << '\n'; // 30
}
Osvědčený postup

Při navrhování konceptů:

- Pojmenujte pojmy podle toho, co JE typ, nikoli podle toho, co funkce potřebuje – použijteSortable, nikoliHasLessThan. Pojmy by měly zachycovat sémantické kategorie.
- Upřednostňujte standardní koncepty knihoven – než napíšete vlastní, zkontrolujte, zda,nebojiž obsahuje to, co potřebujete.
- Udržujte koncepty malé a složitelné – kombinujte malé koncepty s&&místo vytváření monolitických požadavků.
- Používejte pojmenované koncepty přes ad-hocrequires requires – pojmenovaná forma je čitelnější a znovu použitelná.
- Sémantické požadavky dokumentu v komentářích — koncepty kontrolují syntaxi, nikoli sémantiku. Pokud požadujete, aby byloperator+komutativní, řekněte to v komentáři.

Klíčové poznatky
  • Koncepty se nazývají booleovské predikáty, které omezují parametry šablony a poskytují jasné chybové zprávy
  • Výrazrequireskontroluje platné výrazy, návratové typy a vnořené požadavky
  • Čtyři způsoby, jak aplikovat koncepty: omezení na parametr šablony, klauzule required, koncové požadavky neboConcept auto
  • Koncepty tvoří&&a||; subsumpce vybere nejvíce omezené přetížení
  • Standardní knihovna poskytuje koncepty v,a
  • requires requiresje ad-hoc inline omezení – kvůli čitelnosti preferujte pojmenované koncepty

Kvíz — Otestujte své znalosti

(20 XP)

1. Co znamená`void print(std::integral auto value)`?

2. Pokud je koncept A definován jako`concept A = B<T> && C<T>`, co se stane, když se shoduje přetížení s omezením B a A?

3. Why does`requires requires(T a) { a.size(); }`have the keyword 'requires' twice?