Zum Inhalt springen

C++20-Concepts & Constraints

Formuliere mit Concepts präzise Anforderungen an Template-Argumente. Schreibe saubereren generischen Code mit requires-Klauseln, abgekürzten Funktions-Templates und den Concepts der Standardbibliothek.

Das Problem, das Concepts lösen

Vor den Concepts waren Template-Fehlermeldungen kryptisch — std::sort auf einem Typ ohne operator< konnte seitenweise Fehlertext aus den Tiefen der Standardbibliothek erzeugen. Concepts lösen das, indem du die Anforderungen an Template-Parameter von vornherein angibst.

Ein Concept ist ein benanntes boolesches Prädikat, das zur Kompilierzeit ausgewertet wird. Es prüft, ob ein Typ eine Reihe syntaktischer und semantischer Anforderungen erfüllt. Ist ein Concept nicht erfüllt, gibt der Compiler eine klare, knappe Fehlermeldung aus, die genau auf die verletzte Anforderung zeigt — statt einer Wand aus Instanziierungs-Backtraces.

Concepts kamen mit C++20 und sind die bedeutendste Verbesserung der generischen Programmierung seit den Templates selbst.

Concepts definieren

Ein Concept wird mit dem Schlüsselwort concept definiert, gefolgt von einem requires-Ausdruck, der festlegt, welche Operationen ein Typ unterstützen muss. Der requires-Ausdruck kann gültige Ausdrücke, Rückgabetypen, verschachtelte Typen und mehr prüfen.

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
}

requires-Klauseln & abgekürzte Syntax

Es gibt mehrere Wege, ein Concept auf ein Template anzuwenden. Die requires-Klausel steht entweder hinter der Template-Parameterliste oder hinter der Funktionssignatur. Die abgekürzte Syntax für Funktions-Templates verwendet Concept auto als Parametertyp und ist die knappste 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
}

Concepts kombinieren & Standard-Concepts

Concepts lassen sich mit && (Konjunktion) und || (Disjunktion) ganz natürlich kombinieren. Die Standardbibliothek bietet in eine reichhaltige Auswahl:

Concepts der Kernsprache:
- std::same_as — T und U sind derselbe Typ
- std::derived_from — Derived ist von Base abgeleitet
- std::convertible_to — From ist implizit in To umwandelbar
- std::integral, std::floating_point, std::signed_integral

Vergleichs-Concepts:
- std::equality_comparable — unterstützt == und !=
- std::totally_ordered — unterstützt <, >, <=, >=, ==, !=

Objekt-Concepts:
- std::movable, std::copyable, std::semiregular, std::regular

Concepts für Aufrufbares:
- std::invocable — F kann mit Args aufgerufen werden
- std::predicate — F ist ein Prädikat, das bool liefert

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

Concept-Subsumption & Überladungsauflösung

Passen mehrere eingeschränkte Überladungen, wählt der Compiler über die Concept-Subsumption die spezifischste aus. Concept A subsumiert Concept B, wenn die Anforderungen von A die von B logisch implizieren. So wird "Verfeinerung" möglich — ein spezifischeres Concept gewinnt gegen ein allgemeineres.

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
}

Das Muster requires requires

Das etwas ungewöhnliche requires requires taucht auf, wenn du einen spontanen requires-Ausdruck direkt in einer requires-Klausel verwendest, ohne ein benanntes Concept zu definieren. Das erste requires leitet die Constraint-Klausel ein, das zweite beginnt den requires-Ausdruck.

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
}
Best Practice

Beim Entwurf von Concepts gilt:

- Benenne Concepts danach, was der Typ IST, nicht danach, was die Funktion braucht — nimm Sortable statt HasLessThan. Concepts sollten semantische Kategorien abbilden.
- Bevorzuge Concepts der Standardbibliothek — prüfe, bevor du eigene schreibst, ob , oder das Gewünschte bereits mitbringt.
- Halte Concepts klein und kombinierbar — verknüpfe kleine Concepts mit &&, statt monolithische Anforderungen zu bauen.
- Setze benannte Concepts vor spontanes requires requires — die benannte Form ist lesbarer und wiederverwendbar.
- Dokumentiere semantische Anforderungen in Kommentaren — Concepts prüfen Syntax, keine Semantik. Wenn du verlangst, dass operator+ kommutativ ist, schreib es in einen Kommentar.

Wichtigste Erkenntnisse
  • Concepts sind benannte boolesche Prädikate, die Template-Parameter einschränken und für klare Fehlermeldungen sorgen
  • Der requires-Ausdruck prüft gültige Ausdrücke, Rückgabetypen und verschachtelte Anforderungen
  • Vier Wege, Concepts anzuwenden: als Constraint am Template-Parameter, als requires-Klausel, als nachgestelltes requires oder als Concept auto
  • Concepts lassen sich mit && und || kombinieren; die Subsumption wählt die am stärksten eingeschränkte Überladung
  • Die Standardbibliothek stellt Concepts in , und bereit
  • requires requires ist ein spontanes Inline-Constraint — benannte Concepts sind der Lesbarkeit zuliebe vorzuziehen

Quiz — Teste dein Wissen

(20 XP)

1. Was bedeutet `void print(std::integral auto value)`?

2. Concept A ist als `concept A = B<T> && C<T>` definiert. Was passiert, wenn sowohl eine mit B als auch eine mit A eingeschränkte Überladung passt?

3. Warum steht das Schlüsselwort 'requires' in `requires requires(T a) { a.size(); }` zweimal?