C++20-Konzepte: Neue Wege mit Konzepten
Seite 2: Verbesserte Fehlermeldungen durch Konzepte
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 vonstd::true_type
? - Was genau sind
std::false_type
undstd::true_type
? std::void_t
inB
– was war das noch gleich?- Bei der Kaskade von
decltype
undstd::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.