Auf template vergessen?

Wenn man so etwas sieht wie:

1error: no match for 'operator<' (operand types are 
2'<unresolved overloaded function type>' and 'SomeType')

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

1template<class T> struct Wrapper : public T
2{
3public:
4  void do_something()
5  {
6          do_something_special();
7  }
8}

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:

1        T::do_something_special();
2        this->do_something_special();

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.