C++20-Konzepte: Neue Wege mit Konzepten

Seite 3: Neue Möglichkeiten durch die Trailing Requires Clause

Inhaltsverzeichnis

Die Trailing Requires Clause ermöglicht das Einschränken von Methoden eines Klassen-Templates, ohne dass sie selbst ein Template werden müssen.

Ein Beispiel ist eine Wrapper-Klasse wie std::optional, die das Verhalten des zu umschließenden Datentyps nachahmen soll. Eine der Aufgaben ist es, einen Default-Konstruktor nur dann bereitzustellen, wenn der umschlossene Datentyp selbst einen besitzt. Im nachfolgenden Listing ist eine stark vereinfachte Version in Form der Klasse Wrapper zu sehen.

#include <type_traits>

template<typename T>
class Wrapper
{
public:
  template<typename std::enable_if<
             std::is_default_constructible_v<T>,
             T>::type = 0>
  Wrapper()
  {}

  Wrapper(T) {}

  // more access functions
};

static_assert(
  std::is_default_constructible_v<Wrapper<int>>);

Der enable_if für den Default-Konstruktor sieht richtig aus und funktioniert wunderbar in Kombination mit int oder anderen Datentypen, die einen Default-Konstruktor mitbringen. Fehlt Letzterer bei einem verwendeten Datentyp, kompiliert der Code jedoch nicht mehr, unabhängig davon, ob Wrapper standardmäßig initialisiert wird oder nicht.

Der Grund hierfür ist, dass der Default-Konstruktor kein Template ist. Ohne einen Template-Parameter, beispielsweise in Form eines Arguments für den Konstruktor, ist die Formulierung eines Templates aber nicht möglich. Eine leicht abgewandelte Form dieses Problems ist der Kopierkonstruktor. Dabei existiert zwar das benötigte Argument für die Formulierung eines Templates, aber die Form eines Kopierkonstruktors hat gemäß dem C++-Standard keinen Template-Kopf. Wird er dennoch hinzugefügt, ist das Ergebnis ein Konvertierungskonstruktor. Zudem ist es dann schwer, diesen Konstruktor auf exakt den Datentyp T festzulegen. Somit tut sich bei einer im Grunde genommen einfachen Aufgabe ein ganzer Sumpf an Problemen auf.

Davon abgesehen, dass die gezeigte und vermeintlich einfache Umsetzung nicht funktioniert, enthält sie enable_if, std::is_default_constructible_v und einen Default-Template-Parameter.

Das ist ein Fall für die Trailing Requires Clause der C++20-Konzepte. Sie erlaubt das Aktivieren oder Deaktivieren einer Funktion, ohne dass diese selbst ein Template sein muss. Da Wrapper ein Klassen-Template ist, steht der erforderliche Template-Parameter T für die Auswertung von std::is_default_constructible_v zur Verfügung. Durch die Trailing Requires Clause ist es zudem möglich, dem Compiler die Bereitstellung der Default-Implementierung für den Konstruktor mittels =default zu übertragen:

template<typename T>
class Wrapper
{
public:
  Wrapper() requires(
    std::is_default_constructible_v<T>) = default;

  Wrapper(T) {}
};

struct A {
  A() = delete;
  ;
};

static_assert(
  std::is_default_constructible_v<Wrapper<int>>);
static_assert(
  not std::is_default_constructible_v<Wrapper<A>>);

Als Erstes ist sicherlich zu erwähnen, dass die hier gezeigte Version im Gegensatz zu der vorherigen funktioniert. Ein weiteres Plus ist die Lesbarkeit. Dank requires erübrigt sich enable_if.

Diese Variante lässt sich eins zu eins auf den Kopierkonstruktor übertragen, wodurch das komplizierte Filtern auf den richtigen Datentyp entfällt. Beim Kopierkonstruktor ist ebenso wie beim Default-Konstruktor jeweils eine Basisklasse nötig. Von dieser erbt der Wrapper, um die Funktionalität ohne C++20 zu realisieren. Insgesamt erzeugt das wesentlich mehr und vor allem schwerer zu verstehenden Code. C++20 ist dahingehend ein großer Gewinn.

Mit der Trailing Requires Clause lässt sich auch der Destruktor aktivieren oder deaktivieren, in Abhängigkeit von den Eigenschaften eines Datentyps.

Zusammengefasst lässt sich die Trailing Requires Clause auf die folgenden Konstruktoren anwenden, die nicht als Template formuliert werden können:

  • Default-Konstruktor
  • Kopierkonstruktor
  • Destruktor