Skip to content

Latest commit

 

History

History
251 lines (175 loc) · 7.01 KB

File metadata and controls

251 lines (175 loc) · 7.01 KB

Konzepte (Concepts) und Einschränkungen (Requirements) für Funktionen

Zurück


Quellcode


Inhalt


Allgemeines

Templates eignen sich sehr gut zum Schreiben von Code, der mit unterschiedlichen Datentypen funktioniert.

Mit dem Konzept von „C++ Concepts” wiederum können die Parameter für Templates durch boolesche Prädikate validiert werden.

Auf diese Weise kann man erreichen, dass Funktionstemplates nur für sinnvolle Template Parameter instanziiert werden.


Variante 1: requires-Klausel zwischen der Template-Parameterliste und dem Rückgabetyp der Funktion

Bei der ersten der vier vorgestellten Möglichkeiten verwenden wir die requires-Klausel zwischen der Template-Parameterliste und dem Rückgabetyp der Funktion.

Der Rückgabetyp der Funktion kann dann auto oder einer der Template Parameter sein.

Beispiel:

01: template <typename T>
02: concept Numerical = std::integral<T> or std::floating_point<T>;
03: 
04: template <typename T>
05:     requires Numerical<T>
06: auto add(T a, T b)
07: {
08:     return a + b;
09: }

Natürlich kann ein Funktionstemplate mehrere Template Parameter haben. Im nachfolgenden Beispiel lassen sich Werte unterschiedlichen Typs addieren:

template <typename T, typename U>
    requires Numerical<T> and Numerical<U>
auto add(T a, U b)
{
    return a + b;
}

Das Schlüsselwort requires ist neu ab C++ 20. Es wendet Einschränkungen (Constraints) auf ein Template bzw. auf die Parameter eines Templates an. Numerical ist der Name eines Konzepts, das nur Integer- und Gleitkommatypen akzeptiert.

Versucht man, derartige Templates mit unzulässigen Datentyp Parametern zu übersetzen, erhält man verständliche Übersetzungsfehler:

The concept 'Numerical<std::string>' evaluated to false	
The associated constraints are not satisfied

Man kann die Requirements der Template Parameter auch in einer Art „inline”–Notation formulieren. In diesem Fall entfällt die separate concept-Definition:

01: template <typename T>
02:     requires std::integral<T> or std::floating_point<T>
03: auto add(T a, T b)
04: {
05:     return a + b;
06: }

Zur Nomenklatur:

Der C++–Standard verwendet die Begriffe Konjunktion, Disjunktion und atomar, um Arten von Ausdrücken zu beschreiben, die zum Erstellen einer requires-Klausel / eines Konzepts verwendet werden können:

  • Eine Einschränkungs-Konjunktion (constraint conjunction) wird gebildet, indem der &&-Operator (and-Operator) auf zwei Einschränkungen angewendet wird.

  • Eine Einschränkungs-Disjunktion (constraint disjunction) wird gebildet, indem der ||-Operator (or-Operator) auf zwei Einschränkungen angewendet wird.

  • Eine atomare Einschränkung bezeichnet einen Ausdruck, der einen boolschen Typ zurückgibt und nicht weiter zerlegt werden kann.

  • Der logische Not-Operator (!, not) wird verwendet, um den negierten Wert einer Einschränkung zu erhalten.


Variante 2: Nachgestellte requires-Klausel

Das Schlüsselwort requires tritt in dieser Variante in der Funktionssignatur in Erscheinung:

Beispiel:

01: template <typename T>
02: auto add(T a, T b) requires Numerical<T>
03: {
04:     return a + b;  
05: }

Wir erhalten das gleiche Ergebnis wie bei der requires-Klausel zuvor, wir haben die Klausel nur mit einer anderen Syntax geschrieben.

In dieser Definition können wir wieder nicht zwei Zahlen unterschiedlichen Typs addieren. Wir müssten die Template Definition ähnlich wie zuvor ändern:

01: template <typename T, typename U>
02: auto add(T a, U b) requires Numerical<T> and Numerical<U>
03: {
04:     return a + b;
05: }

Variante 3: Template-Parameter mit Einschränkungen („Constraints”)

Die dritte Art, ein Konzept zu verwenden, ist in der Schreibweise etwas knapper als die vorherigen. Der Bezeichner des Konzepts wird in der Template Deklaration verwendet:

Beispiel:

01: template <Numerical T>
02: auto add(T a, T b)
03: {
04:     return a + b;
05: }

In dieser Schreibweise kann man erkennen, das es keine requires-Klausel mehr gibt. Der Template Parameter – in der Regel T – wird anstelle mit dem Schlüsselwort typename jetzt mit einem Bezeichner für ein Konzept (hier: Numerical) definiert. Wir erzielen das gleiche Ergebnis wie mit den beiden vorherigen Varianten.

Die dritte Variante besitzt einige Einschränkungen.

Wenn man die requires-Klausel wie in der Art von Variante 1 oder 2 verwenden wollte, könnte man einen Ausdruck wie z.B.

requires std::integral<T> or std::floating_point<T>

schreiben. In der dritten Variante geht das nicht.


Variante 4: Abgekürzte Funktionstemplates („Abbreviated Function Templates”)

Für all diejenigen, die es kurz lieben, gibt es die so genannte „Abbreviated Function Templates” Schreibweise:

01: auto add(Numerical auto a, Numerical auto b)
02: {
03:     return a + b;
04: }

Wenn Sie sich für die „Abbreviated Function Templates” Schreibweise entscheiden, können Sie sowohl auf Template Parameter als auch auf requires-Klauseln verzichten.

Sie können concept-Bezeichner direkt dort verwenden, wo die Funktionsargumente aufgelistet sind.

Beachte:

Der concept-Bezeichner wird in dieser Variante ohne Template Parameter verwendet!


requires-Klauseln mit benutzerdefinierten Einschränkungen

Zum Schreiben einer requires-Klausel lassen sich die vordefinierten Merkmale („Traits”) aus dem Header <type_traits> verwenden.

Man kann aber auch seine eigenen Merkmale definieren. Diese müssen dann den Typ constexpr bool zurückgeben.

Beispiel:

template<typename T>
constexpr bool isGreaterThanWord{ sizeof(T) > 2 };

Damit ließe sich dann ein Konzept so definieren:

// using <type_traits>
template <typename T>
concept GreatIntegral = std::is_integral<T>::value and isGreaterThanWord<T>;

Eine mögliche Anwendung könnte dann so aussehen:

template<GreatIntegral T>
T incrementByOne(const T& arg) {
    return arg + 1;
}

Literaturhinweise

Ideen und Anregungen zu den Beispielen aus diesem Abschnitt stammen aus

4 ways to use C++ Concepts in Functions (abgerufen am 09.04.2023).

von Sandor Dargo.


Zurück