C++20-Konzepte: Neue Wege mit Konzepten

Seite 2: Verbesserte Fehlermeldungen durch Konzepte

Inhaltsverzeichnis

Ein bedeutender Unterschied zwischen Konzepten und dem initial erwähnten enable_if sind die Compiler-Fehlermeldungen. Ein Compiler kann gut verstehen, was ein Konzept ausdrückt. Dagegen ist enable_if ein Konstrukt wie viele andere. Der Compiler versteht nicht, dass damit etwas aktiviert oder deaktiviert werden soll.

Als Beispiel hierfür eignet sich die Prüfung, ob ein Datentyp das Interface eines STL-Containers erfüllt. Im Grunde genommen besteht das Interface eines STL-Containers aus dem Vorhandensein der Typen

  • value_type
  • size_type
  • allocator_type
  • iterator
  • const_iterator

sowie der Funktionen

  • size
  • begin
  • end
  • cbegin
  • cend

Das ist recht überschaubar. Eine mögliche Implementierung in C++17 ist in diesem Listing zu sehen:

template<typename T, typename U = void>  // A
struct is_container : std::false_type {};

template<typename T>
struct is_container<
  T,
  std::void_t<typename T::value_type,  // B
              typename T::size_type,
              typename T::allocator_type,
              typename T::iterator,
              typename T::const_iterator,
              decltype(std::declval<T>().size()),
              decltype(std::declval<T>().begin()),
              decltype(std::declval<T>().end()),
              decltype(std::declval<T>().cbegin()),
              decltype(std::declval<T>().cend())>>
: std::true_type {};

struct A {};

static_assert(!is_container<A>::value);  // C
static_assert(is_container<std::vector<int>>::value);

Die offenen Fragen sind vor allem für C++-Einsteiger bei diesem Ansatz vielfältig:

  • Wieso existiert der Name is_container zweimal?
  • Wieso erbt dieses Konstrukt einmal von std::false_type und das andere Mal von std::true_type?
  • Was genau sind std::false_type und std::true_type?
  • std::void_t in B – was war das noch gleich?
  • Bei der Kaskade von decltype und std::declval bleibt die Frage, was mehr stört – die Wiederholungen oder dass unklar ist, was sie tun.
  • Und das Beste zum Schluss: Der Einsatz erfordert ::value, wenn nicht noch mehr Code es mithilfe eines Variablen-Templates versteckt.

Für Einsteiger sind all diese Fragen eine Hürde. Die Formulierung, was zu prüfen ist, findet sich in lesbarem Text in der Beschreibung der oben gezeigten Prüfung, ob ein Datentyp das Interface eines STL-Containers erfüllt. Zum Verständnis des Codes braucht es dagegen einiges an Wissen. Zum Aufatmen zeigt das nächste Listing die Variante in C++20. Hier ist lesbarer Code zu sehen, der einfach und verständlich ist und kein tiefes C++- und STL-Wissen voraussetzt.

template<typename T>
concept container = requires(T t)
{
  typename T::value_type;
  typename T::size_type;
  typename T::allocator_type;
  typename T::iterator;
  typename T::const_iterator;
  t.size();
  t.begin();
  t.end();
  t.cbegin();
  t.cend();
};

struct A {};

static_assert(not container<A>);
static_assert(container<std::vector<int>>);

Diese Version sieht beinahe so aus wie der zuerst formulierte reine Text. Es bleibt die Frage, wie es um die Fehlermeldungen steht. Beim Verwenden der C++17-Implementierung is_container gibt Clang folgende Fehlermeldung aus:

<source>:32:1: error: static_assert failed due to requirement 'is_container<std::array<int, 5>, void>::value' static_assert(is_container<std::array<int, 5>>::value); ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 error generated. Compiler returned: 1 

Sie zeigt an, dass static_assert funktioniert und std::array das Interface is_container nicht erfüllt. Auf der Habenseite kompiliert der offensichtlich falsche Code nicht und schafft es damit nie zum Kunden – ein essenzieller Punkt. Darüber hinaus teilt die Fehlermeldung keine weiteren Informationen, etwa wieso std::array kein Container ist.

C++20 und das Konzept container helfen an dieser Stelle weiter. Beim Aufruf von container mit einem std::array in static_assert zeigt Clang folgende Fehlerausgabe:

<source>:26:1: error: static_assert failed static_assert(container<std::array<int, 5>>); ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ <source>:26:15: note: because 'std::array<int, 5>' does not satisfy 'container' static_assert(container<std::array<int, 5>>);              ^ <source>:9:17: note: because 'typename T::allocator_type' would be invalid: no type named 'allocator_type' in 'std::array<int, 5>'    typename T::allocator_type;                ^ 1 error generated. Compiler returned: 1 

Nach wie vor ist zu sehen, dass static_assert seinen Job erledigt, indem er verhindert, dass der Code kompiliert. Nun kommt die Mächtigkeit von Konzepten ins Spiel: Der Compiler versteht jetzt, dass es sich bei den einzelnen Anforderungen um solche handelt und weiß exakt, welche Anforderung nicht erfüllt ist. Das zeigt die Ausgabe, die mitteilt, dass der Container std::array über keinen Typ allocator_type verfügt. Das ist logisch, denn der Grundgedanke von std::array ist, dass es zur Compile-Zeit alloziert wird. In der C++17-Variante ist das ein schwer auffindbarer Fehler. Bei std::array ist weitgehend bekannt, dass es nicht alloziert. Doch vor allem in einer großen und womöglich neuen Codebasis mit weniger bekannten Datentypen bieten die Fehlermeldungen von C++20 und Konzepten einen großen Gewinn.