Auf template vergessen?

Wenn man so etwas sieht wie:

error: no match for 'operator<' (operand types are 
'<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 gewechsel, 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 instanziert werden. Ein template<class T> class X; bleibt also so lange unberührt, bis das erste Mal ein X<sometype_t> instanziert 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 Instanzierung 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

template<class T> struct Wrapper : public T
{
public:
  void do_something()
  {
	do_something_special();
  }
}

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 instansiert (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:

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

Und ein ähnliches Problem ergibt sich bei Methoden, die direkt durch templates Instanziert werden, oder diese nur “weiterleiten”. Mein Beispiel sah etwa so aus:

enum StateEnum
{
  State_1,
  ...
};

struct Foo
{
  template<int STATE> void interpret()
  {
    ...
  }
};

template<int STATE> struct Bar
{
  void do_something(Foo& foo)
  {
    foo.interpret<STATE>();	// fails
  }
};

int main()
{
  Foo foo;
  Bar<State_1> bar;
  bar.do_something(foo)
  return 0;
}

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:

  instance.template methodname<template_arguments>(func_arguments);

Und das Beispiel oben muss dann so aussehen:

  foo.template interpret<STATE>();

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 Methodenaufrufes 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.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!