Template macht Compiler-Probleme!! (VS 2008 SP1)



  • Der folgende Code compiliert seltsamerweise nicht in VS 2008:

    [cpp]template <typename ArrayElement>
    class Array
    {
    public:
    	[b]virtual[/b] void split( Array<Array*> *result = 0) // see comments below
    	{
    		if( result != 0)
    		{
    			result->clear();
    			// do something to generate the result...
    		}
    	}
    	void clear()
    	{
    		// empty the array
    	}
    };
    
    void main(void)
    {
    	Array<int> a;
    	a.split(); // the result does not matter for this compiler problem demo
    }[/cpp]
    

    Dies ist nur eine vereinfachte (und funktional nutzlose) Version meiner tatsächlichen Array-Implementierung, aber das Problem des Compilers wird hier schnell offenbar. Irgendwie meint der Compiler hier eine Art "Template-Rekursion" produzieren zu müssen 😡 Ich verstehe jedoch nicht wirklich, warum dies auftritt.

    Interessant ist dann noch, wenn man das Keyword "virtual" in diesem Beispiel weglässt, daß dann der Code auf einmal compiliert... 😕

    Ich befürchte einen Compiler-Bug und hoffe auf Eure werte Hilfe, wie ich meine Array-Klasse am sinnvollsten retten kann. Der Datentyp des Rückgabe-Parameters bei der Funktion "split" soll aber auf jeden Fall so erhalten bleiben und die Funktion soll auf jeden Fall auch "virtual" sein.

    Warte sehnsüchtig auf Eure Antworten,
    Oli


  • Mod

    wie sieht es mit

    virtual void split( Array<Array<ArrayElement>*> *result = 0)
    

    aus?



  • Hab ich auch schon ausprobiert. Gleiches Ergebnis...


  • Mod

    Kannst du "compiliert seltsamerweise nicht" etwas erläutern? Zum Beispiel mit einer Fehlermeldung?


  • Mod

    Ich habe ein bisschen gespielt und denke, dass das kein Bug ist, sondern ein Fall für 14.7.1/9 ist

    An implementation shall not implicitly instantiate a function template, a member template, a non-virtual member function, a member class or a static data member of a class template that does not require instantiation. It is unspecified whether or not an implementation implicitly instantiates a virtual member function of a class template if the virtual member function would not otherwise be instantiated. The use of a template specialization in a default argument shall not cause the template to be implicitly instantiated except that a class template may be instantiated where its complete type is needed to determine the correctness of the default argument. The use of a default argument in a function call causes specializations in the default argument to be implicitly instantiated.

    Lässt du die Definition von split (nicht aber die Definition) weg, entsteht dieses Problem nicht (nat. gibts dann einen Linkerfehler). Jede Instantiierung der Funktion split instantiiert gleichzeitig die Spezialisierung von Array, bei dem ArrayElement jeweils Array<ArrayElement>* ist (wegen result->clear()). Verursacht diese Instantiierung nun wiederum die Instantiierung der Memberfunktion split, haben das Problem endloser Instantiierung - leider wird dieser Fall allerdings compilerintern nicht sauber mit der richtigen Fehlermeldung quittiert.

    Als möglichen Ausweg sehe das Anbieten einer Spezialierung für eine Rekursionstiefe, die garantiert nie gebraucht wird.

    template <typename ArrayElement>
    class Array
    {
    public:
        virtual void split( Array<Array*> *result = 0)
        {
            if( result != 0)
            {
                result->clear();
            }
        }
        void clear()
        {
        }
    };
    template <typename ArrayElement>
    class Array<Array<Array<Array<Array<Array<ArrayElement>*>*>*>*>*>
    {
    public:
        void clear() {}
    };
    
    int main()
    {
        Array<int> a;
        a.split();
    }
    


  • versuch das mal im VS05 SP1 zu kompilieren, da kannst du die Rekursion live erleben 😃

    Was willst mit du mit der "virtual void split( Array<Array*> ..." eigentlich genau erreichen?



  • SeppJ schrieb:

    Kannst du "compiliert seltsamerweise nicht" etwas erläutern? Zum Beispiel mit einer Fehlermeldung?

    Naja, der Compiler versucht es schon zu compilieren, da syntaktisch nichts auszusetzen ist! Und eigentlich merkt er dabei gar nicht, daß er in einer Rekursion im Kreis rennt! Die erste Warning, die dann auftritt ist:

    1>------ Erstellen gestartet: Projekt: Array, Konfiguration: Debug Win32 ------
    1>Kompilieren...
    1>main.cpp
    1>.\main.cpp(17) : warning C4503: '$vftable': Die Länge des ergänzten Namens wurde überschritten. Der Name wurde gekürzt.
    1> .\main.cpp(9): Siehe Verweis auf die Instanziierung der gerade kompilierten Klassen-template "Array<ArrayElement>".
    1> with
    1> [ ............

    Den "Rest" lasse ich lieber weg, denn der Compiler kommt niemals zu einem Ende 🙄 Das führt dann zu der genannten Unmöglichkeit das zu compilieren.



  • Pellaeon schrieb:

    versuch das mal im VS05 SP1 zu kompilieren, da kannst du die Rekursion live erleben 😃

    Was willst mit du mit der "virtual void split( Array<Array*> ..." eigentlich genau erreichen?

    Ich will damit das Array in mehrere Unter-Arrays zergliedern, ohne das Original-Array zu verändern (in meiner "realen" Version ist die Funktion auch const). Ähnlich der Funktion "Split", wie man sie bei Strings kennt. Im synthetischen Beispiel von oben jedoch hab ich ein paar Parameter und die Implementierung weggelassen, so daß man das Splitting sicherlich nicht mehr wirklich erkennen kann. Aber für das Compiler-Problem war das unerheblich.

    Durch den Split entstehen dann letztlich neue Arrays vom selben Typ wie das ursprüngliche, die ich sinnvollerweise auch als Array von Arrays zurückgeben will und nicht durch irgendein anderes Konstrukt. Der Aufrufer braucht sich dann nur mit der Array-Klasse zu beschäftigen und kann alles gleichartig verarbeiten.
    Ich denke das macht alles schon Sinn, jedoch mag der Compiler von VS 2008 SP1 bei Verwendung des Keywords "virtual" dann nicht mehr. Auf jeden Fall will ich die Klasse für jede Member-Funktion ableitbar haben und keine Wrapper oder dergleichen verwenden müssen.



  • camper schrieb:

    Ich habe ein bisschen gespielt und denke, dass das kein Bug ist, sondern ein Fall für 14.7.1/9 ist

    An implementation shall not implicitly instantiate a function template, a member template, a non-virtual member function, a member class or a static data member of a class template that does not require instantiation. It is unspecified whether or not an implementation implicitly instantiates a virtual member function of a class template if the virtual member function would not otherwise be instantiated. The use of a template specialization in a default argument shall not cause the template to be implicitly instantiated except that a class template may be instantiated where its complete type is needed to determine the correctness of the default argument. The use of a default argument in a function call causes specializations in the default argument to be implicitly instantiated.

    Lässt du die Definition von split (nicht aber die Definition) weg, entsteht dieses Problem nicht (nat. gibts dann einen Linkerfehler). Jede Instantiierung der Funktion split instantiiert gleichzeitig die Spezialisierung von Array, bei dem ArrayElement jeweils Array<ArrayElement>* ist (wegen result->clear()). Verursacht diese Instantiierung nun wiederum die Instantiierung der Memberfunktion split, haben das Problem endloser Instantiierung - leider wird dieser Fall allerdings compilerintern nicht sauber mit der richtigen Fehlermeldung quittiert.

    Als möglichen Ausweg sehe das Anbieten einer Spezialierung für eine Rekursionstiefe, die garantiert nie gebraucht wird.

    template <typename ArrayElement>
    class Array
    {
    public:
        virtual void split( Array<Array*> *result = 0)
        {
            if( result != 0)
            {
                result->clear();
            }
        }
        void clear()
        {
        }
    };
    template <typename ArrayElement>
    class Array<Array<Array<Array<Array<Array<ArrayElement>*>*>*>*>*>
    {
    public:
        void clear() {}
    };
    
    int main()
    {
        Array<int> a;
        a.split();
    }
    

    Danke für Deine Antwort 🙂 Interessant finde ich hierbei, daß

    [cpp]template <typename ArrayElement>
    class Array<Array<Array<Array<Array<Array<ArrayElement>*>*>*>*>*>
    {
    public:
        void clear() {}
    }; [/cpp]
    

    eine explizite Definition für eine Spezialisierung darstellt. Mir war nicht bekannt, daß ich beim "Tag" (Klassennamen) der Klasse die Spezialisierung eines Templates verwenden kann und damit eine Spezialisierung des originalen Templates explizit verursache!

    Allerdings hat diese Methode auch Nachteile, die mir nicht so sehr gefallen:

    - Dieser "Stopper" erzeugt entweder 100% Code-Duplizität oder ist schlicht sinnlos und nicht direkt verwendbar, weil ich ihn mit Dummy-Code fülle.
    - Alle Template-Spezialisierung zwischen dem "Stopper" und dem Ausgangspunkt werden IMMER als Code erzeugt und erhöhen zumindest auch noch die Compilezeit. Evtl. blähen sie auch das Binary auf, falls der Linker den erzeugten Code nicht wieder wegwirft.

    Abgesehen davon ist es immerhin eine Lösung, die mir aktuell noch am "sinnvollsten" erscheint. Allerdings will mir nicht in den Kopf, warum dieses vergleichsweise einfache Problem für den Compiler unlösbar ist? Im Endeffekt gibt es keinen Grund den Code rekursiv immer weiter zu generieren, da dieser faktisch nirgends im Rest des Codes in dieser Tiefe verwendet wird! Warum also stoppt der Compiler nicht dort, wo kein Aufruf mehr and die entsprechende Spezialisierung existiert?


  • Mod

    Virtuelle Funktionen normaler Klassen, die nicht rein virtuell sind, müssen immer definiert werden unabhängig davon, ab sie überhaupt aufgerufen werden (allerdings wird keine Diagnose bei Verletzung verlangt).
    Der zitierte Teil des Standards überträgt diese Regel im Grunde auf Templates.
    Wenn die Funktion irgendwo definiert sein soll, muss sie logischerweise in irgendeiner ÜE instantiiert werden - im Zweifel eben in jeder ÜE, in der das möglich ist.
    Schließlich müssen wir auch für jede solche Klasse ein vtable anlegen, die auf diese jeweilige virtuelle Funktion verweist und mit Linkerfehler wollen uns auch nicht plagen.

    An sich kann ich mir allerdings auch nicht vorstellen, dass virtuell in diesem Fall überhaupt sinnvoll ist.
    Schließlich wird man in einem Derived ja eher ein Derived<Derived*>* erwarten, nur haben dieser Zeiger nichts mit Array<Array*>* zu tun.



  • camper schrieb:

    Virtuelle Funktionen normaler Klassen, die nicht rein virtuell sind, müssen immer definiert werden unabhängig davon, ab sie überhaupt aufgerufen werden (allerdings wird keine Diagnose bei Verletzung verlangt).
    Der zitierte Teil des Standards überträgt diese Regel im Grunde auf Templates.
    Wenn die Funktion irgendwo definiert sein soll, muss sie logischerweise in irgendeiner ÜE instantiiert werden - im Zweifel eben in jeder ÜE, in der das möglich ist.
    Schließlich müssen wir auch für jede solche Klasse ein vtable anlegen, die auf diese jeweilige virtuelle Funktion verweist und mit Linkerfehler wollen uns auch nicht plagen.

    Ja, das macht absolut Sinn. Sicher der Grund für das Problem des Compilers.

    An sich kann ich mir allerdings auch nicht vorstellen, dass virtuell in diesem Fall überhaupt sinnvoll ist.
    Schließlich wird man in einem Derived ja eher ein Derived<Derived*>* erwarten, nur haben dieser Zeiger nichts mit Array<Array*>* zu tun.

    Eine mögliche Ableitung ist "String" (unstreitbar eine Spezialform des allgemeinen Arrays), die dann von Array<char> abgeleitet sein wird:

    [cpp]class String : public Array<char>
    {
        ...
    }[/cpp]
    

    Damit macht es absolut Sinn einen Parameter vom Type Array<Array*> für die split-Funktion zu haben, der in der konkreten Ausprägung des Templates dann vom Typ Array<Array<char>*> ist und da String von Array<char> abgeleitet ist funktioniert das auch wunderbar! Aus diesem Grund macht das schon Sinn die Member-Funktionen (wie "split") virtuell zu haben. Ein String hat ja z.B. noch die terminierende "0" am Ende, was nicht für den Array allgemein gilt, also werden manche (aber nicht alle) Funktionen etwas anders arbeiten müssen.


  • Mod

    o.kauert schrieb:

    Eine mögliche Ableitung ist "String" (unstreitbar eine Spezialform des allgemeinen Arrays), die dann von Array<char> abgeleitet sein wird:

    hm...

    o.kauert schrieb:

    Ein String hat ja z.B. noch die terminierende "0" am Ende, was nicht für den Array allgemein gilt, also werden manche (aber nicht alle) Funktionen etwas anders arbeiten müssen.

    Das scheint mir widersprüchlich zu sein.
    Wenn die 0 Teil des Arrays ist, ist die Identität nicht gewahrt, weil ja für gewöhnlich jedes Element eines Arrays verändert werden kann, für die terminierende 0 des Strings gilt das aber wohl nicht. Ist die 0 nicht Teil des Arrays, sehe ich wiederum nicht, wieso du eine anderes split brauchst.
    Etwas anders sieht es aus, wenn wir nur konstante Arrays und Strings haben - dann sehe ich allerdings keinen Mehrwert mehr darin, überhaupt eine separate Arrayklasse zu haben.

    Ich habe noch nie ein überzeugendes Beispiel gesehen, in dem das Ableiten von einer konkreten Klasse (d.h. einer Klasse, von der erwartet wird, dass wir davon vollständige Objekte erstellen) nichttrivial ist und nicht gleichzeitig das LSP verletzt.



  • o.kauert schrieb:

    Eine mögliche Ableitung ist "String" (unstreitbar eine Spezialform des allgemeinen Arrays), die dann von Array<char> abgeleitet sein wird:

    [cpp]class String : public Array<char>
    {
        ...
    }[/cpp]
    

    Du kannst nicht vom allgemeinen Sprachgebrauch ausgehen, wenn es um Vererbung bei der Programmierung geht. Vielmehr musst du dir die Frage stellen, "kann ich jeden String wie ein Array<char> verwenden?" Falls die Arrays immutable sind, geht das vielleicht sogar noch, auch wenn du das 0-byte forderst.

    Aber sobald du zulässt, dass Arrays manipuliert werden, kommst du auf die ganzen Probleme: Du darfst dann einen String nicht so abändern, wie jedes Array<char>. Falls du das nicht vollständig hinter der Schnittstelle verbergen kannst, halte ich die Ableitung für illegal.


Anmelden zum Antworten