Rückgabewerte bei Schnittstellen erweitern



  • Hallo Community!

    Ich habe die letzten 6 Jahre in PHP entwickelt und möchte nun meinen Horizont erweitern um C++. Aufgrund der schwachen Typisierung von PHP ist es möglich, unterschiedliche Datentypen von einer Methode wiederzugeben. In der Vererbung ist dies recht praktisch doch C++ bietet so etwas in dem Sinne nicht dank der starken Typisierung. Soweit so gut, so weit so fein. 🙂

    Eine Möglichkeit bieten hier Templates und genau bei diesen bin ich jetzt auf zwei Verständnisprobleme gestoßen, bei dem ich hoffe, dass mir diese einer erläutern kann.

    Als ein Beispiel habe ich mir mal ein kleines Testprogramm geschrieben, bei dem ich ein Iterator Aggregate Design Pattern implementiert habe, wie es in PHP an vielen Stellen benutzt wird. Ich weiß, C++ hat ein viel besseres Iterator Design implementiert, aber für mein Verständnis ist dieses PHP- und Java-like erst einmal besser. 🙂

    So hier der Code und danach meinen Problemen:

    #include <iostream>
    
    // ---------------------------------------------------------------------------
    // Abstraktion
    // ---------------------------------------------------------------------------
    template<typename T> 
    class IteratorInterface
    {
    public:
    	virtual T* current(void) = 0;
    	virtual unsigned long key(void) const = 0;
    	virtual void next(void) = 0;
    	virtual void rewind(void) = 0;
    	virtual bool valid(void) const = 0;
    };
    
    template<typename T> 
    class IteratorAggregateInterface
    {
    public:
    	virtual IteratorInterface<T>* iterator(void) = 0;
    };
    
    // ---------------------------------------------------------------------------
    // Konkretisierung
    // ---------------------------------------------------------------------------
    class Stack : 
        public IteratorAggregateInterface<unsigned long>
    {
    	friend class StackIterator;
    
    private:
    	unsigned long data[10];
    	unsigned long index;
    
    public:
    	Stack(void) : index(0) 
        {
            std::memset(data, 0, sizeof(data));
        }
    
    	const Stack& push(const unsigned long& value)
    	{
    		data[index++] = value;
    		return *this;
    	}
    
    	// StackIterator* iterator(void); // Fehler/Frage 1
    	IteratorInterface<unsigned long>* iterator(void);
    };
    
    class StackIterator : 
        public IteratorInterface<unsigned long>
    {
    private:
    	Stack* stack;
    	unsigned long index;
    
    public:
    	StackIterator(Stack* stack) : 
    		index(0), 
    		stack(stack) 
    	{
    	}
    
    	unsigned long* current(void)	{ return &stack->data[index]; }
    	unsigned long key(void) const	{ return index; }
    	void next(void)					{ ++index; }
    	void rewind(void)				{ index = 0; }
    	bool valid(void) const			{ return index < stack->index; }
    };
    
    // StackIterator* Stack::iterator(void) // Fehler/Frage 1
    IteratorInterface<unsigned long>* Stack::iterator(void)
    {
    	return new StackIterator(this);
    }
    
    // ---------------------------------------------------------------------------
    // Testprogramm
    // ---------------------------------------------------------------------------
    int main(void)
    {
    	Stack stack;
    
    	stack.push(2);
    	stack.push(3);
    	stack.push(4);
    	stack.push(5);
    
    	// StackIterator* i = stack.iterator();	// Fehler/Frage 2
    	IteratorInterface<unsigned long>* i = stack.iterator();
    
    	for (i->rewind(); i->valid(); i->next())
    	{
    		std::cout << (*i->current()) << std::endl;
    	}
    
    	delete i;
    }
    

    Wie man hier sehen kann habe ich ein abstraktes Iterator Aggregate Design Pattern implementiert, welches ich als einheitliche Schnittstelle für Iteratoren und Iterator Aggregates verwenden möchte. Gefolgt wird dies von einer Konkretisierung der Schnittstelle, in dem ich einen Stack implementiert habe mit einem dazugehörigen Iterator.

    Zu meinem Punkt "Fehler/Frage 1"
    In der Klasse IteratorAggregate ist iterator() mit dem Rückgabetyp "IteratorInterface<T>" definiert. Wenn ich nun in der Konkretisierung als Rückgabewert "StackIterator" definiere bekomme ich diesen Kompilierfehler:
    "error C2555: 'Stack::iterator': overriding virtual function return type differs and is not covariant from 'IteratorAggregateInterface<T>::iterator'"

    Dies hat denke ich damit zu tun, dass die Schnittstellendefintion überschrieben wurde. Was ich jetzt nicht verstehe ist, dass IteratorIterface eine Untermenge von StackIterator ist und diese Definition eigentlich nur erweitert wurde und IteratorInterface also beinhaltet. Dadurch sollte diese "Erweiterung" doch eigentlich möglich sein? Gibt es hierfür in C++ eine Implementationsmöglichkeit?

    Zu meinem Punkt "Fehler/Frage 2"
    Eigentlich hängt dieser genau wie Punkt 1 zusammen. Wenn ich "IteratorInterface<unsigned long>" in "StackIterator" ersetze, erlange ich natürlich einen Compiler-Fehler, da StackIterator eine höhere Vererbungsebene hat als IteratorInterface und nur durch Up-Casting erreicht werden kann.

    Meine Hauptfrage bei diesem Beispiel ist, wie kann ich die Konkretisierung auch auf die Rückgabewerte ausweiten, dass ich in einer konkreten Implementierung, in der main-Funktion folgendes Schreiben kann:

    // [...]
    StackIterator* i = stack.iterator();
    // [...]
    

    Ist so etwas überhaupt in C++ möglich?

    Wenn jemand zum Code Anregungen, Kritik oder Erweiterungspotential sieht bitte sagen. Wie schon gesagt, ich weiß das C++ bessere Iterator-Implementierungen besitzt, aber es handelt sich hier ausschließlich um ein Stück Lern-Code. 🙂

    Vielen Dank schon einmal im voraus und einen schönen Abend noch,
    Martin 🙂


  • Mod

    Php-Martin schrieb:

    Zu meinem Punkt "Fehler/Frage 1"
    In der Klasse IteratorAggregate ist iterator() mit dem Rückgabetyp "IteratorInterface<T>" definiert. Wenn ich nun in der Konkretisierung als Rückgabewert "StackIterator" definiere bekomme ich diesen Kompilierfehler:
    "error C2555: 'Stack::iterator': overriding virtual function return type differs and is not covariant from 'IteratorAggregateInterface<T>::iterator'"

    Dies hat denke ich damit zu tun, dass die Schnittstellendefintion überschrieben wurde. Was ich jetzt nicht verstehe ist, dass IteratorIterface eine Untermenge von StackIterator ist und diese Definition eigentlich nur erweitert wurde und IteratorInterface also beinhaltet. Dadurch sollte diese "Erweiterung" doch eigentlich möglich sein? Gibt es hierfür in C++ eine Implementationsmöglichkeit?

    Die Klasse die per Zeiger oder Referenz zurückgegeben wird, muss bereits bei der Deklaration der überschriebenden Funktion vollständig definiert sein (oder aber es ist die Klasse, die gerade definiert wird).

    StackIterator ist an der Stelle, an der die Funktion erstmals in Stack deklariert wird selbst noch unvollständig.
    Die Klassendefinition von StackIterator muss vor der Definition von Stack stehen.



  • @camper
    Ich habe auch schon an eine Forward-Deklaration gedacht, wenn ich diesen Code dann darauf anpasse, also wie folgt erhalte ich ebenfalls einen Compiler-Fehler:

    #include <iostream>
    
    // ---------------------------------------------------------------------------
    // Abstraktion
    // ---------------------------------------------------------------------------
    template<typename T> 
    class IteratorInterface
    {
    public:
    	virtual T* current(void) = 0;
    	virtual unsigned long key(void) const = 0;
    	virtual void next(void) = 0;
    	virtual void rewind(void) = 0;
    	virtual bool valid(void) const = 0;
    };
    
    template<typename T> 
    class IteratorAggregateInterface
    {
    public:
    	virtual IteratorInterface<T>* iterator(void) = 0;
    };
    
    // ---------------------------------------------------------------------------
    // Konkretisierung
    // ---------------------------------------------------------------------------
    class StackIterator;
    class Stack : 
        public IteratorAggregateInterface<unsigned long>
    {
    	friend class StackIterator;
    
    private:
    	unsigned long data[10];
    	unsigned long index;
    
    public:
    	Stack(void) : index(0) 
        {
            std::memset(data, 0, sizeof(data));
        }
    
    	const Stack& push(const unsigned long& value)
    	{
    		data[index++] = value;
    		return *this;
    	}
    
    	 StackIterator* iterator(void); // Fehler/Frage 1
    	//IteratorInterface<unsigned long>* iterator(void);
    };
    
    class StackIterator : 
        public IteratorInterface<unsigned long>
    {
    private:
    	Stack* stack;
    	unsigned long index;
    
    public:
    	StackIterator(Stack* stack) : 
    		index(0), 
    		stack(stack) 
    	{
    	}
    
    	unsigned long* current(void)	{ return &stack->data[index]; }
    	unsigned long key(void) const	{ return index; }
    	void next(void)					{ ++index; }
    	void rewind(void)				{ index = 0; }
    	bool valid(void) const			{ return index < stack->index; }
    };
    
    StackIterator* Stack::iterator(void) // Fehler/Frage 1
    {
    	return new StackIterator(this);
    }
    
    // ---------------------------------------------------------------------------
    // Testprogramm
    // ---------------------------------------------------------------------------
    int main(void)
    {
    	Stack stack;
    
    	stack.push(2);
    	stack.push(3);
    	stack.push(4);
    	stack.push(5);
    
    	// StackIterator* i = stack.iterator();	// Fehler/Frage 2
    	IteratorInterface<unsigned long>* i = stack.iterator();
    
    	for (i->rewind(); i->valid(); i->next())
    	{
    		std::cout << (*i->current()) << std::endl;
    	}
    
    	delete i;
    
    	std::cin.get();
    }
    

    Und mein Fehler:
    error C2555: 'Stack::iterator': overriding virtual function return type differs and is not covariant from 'IteratorAggregateInterface<T>::iterator'


  • Mod

    Php-Martin schrieb:

    @camper
    Ich habe auch schon an eine Forward-Deklaration gedacht,

    Ein bloße Klassen-Deklaration ist keine Definition.



  • @camper
    Ich habe die Implementierung jetzt alle Unterhalb der Klassendefinitionen geschrieben. Jetzt kann ich Problemlos kompilieren und es läuft perfekt. 🙂

    Vielen Dank für deine Hilfe, wenn man von PHP kommt ist das doch noch eine starke Umgewöhnung 😉

    Hast du vielleicht noch 2-3 Tipps für das Iterator Aggregate Design Pattern was ich hier implementiert habe oder wäre das so im grünen Bereich? 🙂


  • Mod

    Php-Martin schrieb:

    Hast du vielleicht noch 2-3 Tipps für das Iterator Aggregate Design Pattern

    Eigentlich nur 1: vergiss das Pattern.
    (Laufzeit-)Polymorphie und Wertsemantik sind in C++ keine orthogonalen Konzepte; willst du beides gleichzeitig haben, wird immer ein Proxy oder etwas ähnliches benötigt.
    Da man bei Objekten wie Iteratoren i.d.R. auf Wertsemantik nicht verzichten kann, muss die Polymorphie weichen.



  • @ camper
    Also im Endeffekt mehr auf die C++ Iterator Pattern konzentrieren. Gibt es dafür irgendwelche Tutorials, wie diese aufgebaut sind in der Implementierung (also nicht in der Verwendung.)?



  • camper schrieb:

    Php-Martin schrieb:

    Hast du vielleicht noch 2-3 Tipps für das Iterator Aggregate Design Pattern

    Eigentlich nur 1: vergiss das Pattern.
    (Laufzeit-)Polymorphie und Wertsemantik sind in C++ keine orthogonalen Konzepte; willst du beides gleichzeitig haben, wird immer ein Proxy oder etwas ähnliches benötigt.
    Da man bei Objekten wie Iteratoren i.d.R. auf Wertsemantik nicht verzichten kann, muss die Polymorphie weichen.

    Hi,

    ich habe gerade diesen (für mich) sehr interessanten Topic gefunden, zu dem ich 2-3 Fragen habe:

    1. Die Aussage, dass "Laufzeit-/Polymorphie und Wertsematik keine orthogonalen Konzepte in C++" sind verstehe ich nicht so richtig. Was ist hiermit im Detail gemeint?

    2. Warum sollte man grundsätzlich bei C++ auf interne Traversierung setzen und weniger auf externe Iteration? Wegen Gründen der Performace?

    3. Ist das hier oben genannte Pattern (welches auch in Java und C# generell eingesetzt wird) grundsätzlich Käse für C++? Mich würde die Begründung dafür interessieren.

    Schönes Wochenende noch!


Anmelden zum Antworten