Auf template vergessen?
« | 20 Oct 2019 | »Wenn man so etwas sieht wie:
denkt man zuerst an eine Klasse, für die der Kleiner-Operator <
falsch oder
gar nicht überladen wurde.
Doch … eigentlich handelt es sich um einen Aufruf einer “ge-template-eten” Methode.
Eigentlich hätte ich den Text auch mit “Neulich in der Firma…” einleiten können. Doch das erlebte Phänomen kenne ich ja aus vielen Bereichen.
Man hat C++ Code der unter Windows (MSVC) sowieso kompiliert und unter Linux mit Compiler X ebenso. Dann wird auf einen neueren Compiler gewechselt, und plötzlich kompiliert etwas nicht mehr.
Doch wenn man glaubt, der neue Compiler hätte einen Bug, so liegt man häufig falsch. Die traurige Wahrheit ist am Ende, dass man falschen Code geschrieben hat, den “nachsichtige” Compiler durchgelassen haben, womit sich die Compiler selbst nicht an den Standard halten.
Der 2-Phasen Lookup
Fast immer wenn Templates unter Windows (MSVC) kompilieren und unter Linux (GCC) nicht, liegt es am 2-Phasen Lookup, den Microsoft aus Kompatibilitätsgründen nicht oder nur unvollständig durchführt.
Microsoft nutzt dabei den Ansatz, dass Templates erst an der Stelle
mitkompiliert werden, wenn sie instanziiert werden. Ein
template<class T> class X;
bleibt also so lange unberührt, bis das erste Mal
ein X<sometype_t>
instanziiert wird.
Und dann wird im Rahmen von sometype_t
die Syntax von X
geprüft.
Der C++ Standard schreibt allerdings vor, dass Templates in 2 Anläufen geprüft werden. Das erste Mal generisch ohne Instanziierung und das 2 Mal mit den typen-abhängigen Informationen.
Und diese “erste Phase” hat ein Problem: Manche Details der Implementierung
sind zu diesem Zeitpunkt noch nicht bekannt.
Nehmen wir das Beispiel
Unser Wrapper
erbt von T
und geht davon aus, dass es in T
eine Methode namens do_something_special
gibt.
Wird Wrapper
zusammen mit einer Klasse instanziiert (Phase 2),
die ein do_something_special
besitzt, ist alles gut.
Doch in Phase 1 stellen wir fest, dass es (noch) keine Funktion
namens do_something_special
gibt und hier bricht der GCC dann ab.
Die Lösung ist, dem Compiler zu sagen, dass die Methode von T
abhängt, und dass kann man beim Aufruf z.B.: mit this
oder T::
tun:
Und ein ähnliches Problem ergibt sich bei Methoden, die direkt durch Templates instanziiert werden, oder diese nur “weiterleiten”. Mein Beispiel sah etwa so aus:
1enum StateEnum 2{ 3 State_1, 4 ... 5}; 6 7struct Foo 8{ 9 template<int STATE> void interpret() 10 { 11 ... 12 } 13}; 14 15template<int STATE> struct Bar 16{ 17 void do_something(Foo& foo) 18 { 19 foo.interpret<STATE>(); // fails 20 } 21}; 22 23int main() 24{ 25 Foo foo; 26 Bar<State_1> bar; 27 bar.do_something(foo) 28 return 0; 29}
Doch der Code endet mit obiger Fehlermeldung …
Es liegt am Aufruf von foo.interpret<STATE>
, denn STATE
ist ebenfalls in der ersten Phase unbekannt. Es könnte eine Zahl sein,
die mit dem Kleiner-Operator <
verglichen wird.
Tja, und wie ruft man jetzt eine ge-template-ete Methode korrekt auf?
Lösung:
1 instance.template methodname<template_arguments>(func_arguments);
Und das Beispiel oben muss dann so aussehen:
1 foo.template interpret<STATE>();
Fazit
Und wieder hat mich so eine Konstellation ein paar Stunden Zeit gekostet.
Problematisch war in erster Linie die tückische Meldung, dass beim
operator<
etwas nicht passt.
Denn da sucht man naturgemäß an anderen Stellen und nicht bei
der Syntax des Methodenaufrufs selbst.
Doch zum Glück hab ich wieder was gelernt:
Das Schlüsselwort template
ist nicht nur zum Deklarieren von Templates da,
sondern auch um Template-Methoden aufzurufen.