Kompatibilität von Funktionszeigern bei Parametern mit Vererbungsbeziehung



  • Hallo zusammen

    Ich habe einen Funktionszeiger dfn , der auf eine Funktion verweist, welche ein Derived* als Parameter nimmt. Nun möchte ich ihn einem Funktionszeiger zuweisen, der ein Base* als Parameter bekommt.

    Dabei kann ich garantieren, dass der Base* -Parameter beim Aufruf immer ein Derived* ist. Ist das Verhalten des Aufrufs definiert?

    class Base { ... };
    class Derived : public Base { ... };
    
    int main()
    {
    	typedef void (*BaseFn)   (Base*);
    	typedef void (*DerivedFn)(Derived*);
    
    	DerivedFn dfn = ...;
    	BaseFn    bfn = reinterpret_cast<BaseFn>(dfn);
    
    	Derived* d = ...;
    	Base*    b = d;
    	bfn(b); // okay?
    }
    

    Ich vermute nicht, aufgrund irgendwelcher Implementierungsfreiheiten. Kennt in diesem Fall jemand einen anderen Weg mit Funktionszeigern? Dass ich das gewünschte Verhalten mit std::function relativ einfach hinkriegen kann, ist mir bewusst.



  • Dass es nicht garantiert ist, sollte dir schon der nötige reinterpret_cast sagen.

    Mir fällt auch nix ein wie das direkt und "schön" gehen sollte.

    Du kannst natürlich ne Trampolin-Funktion basteln. Nur dann musst du der Trampolin-Funktion nen zusätzlichen Parameter mitgeben, nämlich den "Derived*" Funktionszeiger.

    Oder aber du machst die Trampolin-Funktion zu einem Template. Dann muss aber der "Derived*" Funktionszeiger an der Stelle wo du den "Base*" Funktionszeiger erstellen willst compile-time-const sein (da du ihn ja als Template-Parameter verwenden musst).

    Weiters gibt es dann noch die Möglichkeit dynamisch Thunks zu erstellen, nur das ist dann überhaupt nicht mehr portabel. Und natürlich muss man solche Thunks dann auch wieder freigeben.

    EDIT: Base/Derived verwechselt -> korrigiert /EDIT



  • hustbaer schrieb:

    Dass es nicht garantiert ist, sollte dir schon der nötige reinterpret_cast sagen.

    Ja, wobei es sein könnte, dass hierfür explizit Ausnahmen vorgesehen wären. Rein technisch sehe ich auch nicht gerade etwas, was für UB spricht.

    Was ist denn der Anwendungsfall von reinterpret_cast für Funktionszeiger? Wann castet man hin und zurück, bevor man die Funktion aufruft?

    hustbaer schrieb:

    Du kannst natürlich ne Trampolin-Funktion basteln. Nur dann musst du der Trampolin-Funktion nen zusätzlichen Parameter mitgeben, nämlich den "Derived*" Funktionszeiger.

    Oder aber du machst die Trampolin-Funktion zu einem Template. Dann muss aber der "Derived*" Funktionszeiger an der Stelle wo du den "Base*" Funktionszeiger erstellen willst compile-time-const sein (da du ihn ja als Template-Parameter verwenden musst).

    Das Problem ist, dass ich momentan einen Funktionszeiger mit fester Signatur habe. Die zugewiesenen Funktionen können in anderen Funktionszeigern gespeichert sein, also keine Compilezeit-Konstante.



  • #include <iostream>
    struct A_Base { int a; };
    struct B_Base { B_Base() : privdat(-1) {} int b; private: int privdat; };
    struct Derived : A_Base, B_Base { Derived() { a=2; b=3; } };
    
    void print(Derived *b) { std::cout << "sollte 3 sein: " << b->b << '\n'; } 
    
    int main()
    {
      typedef void (*BaseFn)   (B_Base*);
      typedef void (*DerivedFn)(Derived*);
    
      DerivedFn dfn = &print;
      BaseFn    bfn = reinterpret_cast<BaseFn>(dfn);
    
      Derived* d = new Derived;
      B_Base*  b = d;
      bfn(b); // okay?
      delete d;
    }
    

    clang und der gcc geben beide "sollte 3 sein: -1" aus. Jetzt rate mal, ob das, was du machst, definiert ist.



  • Nexus schrieb:

    hustbaer schrieb:

    Dass es nicht garantiert ist, sollte dir schon der nötige reinterpret_cast sagen.

    Ja, wobei es sein könnte, dass hierfür explizit Ausnahmen vorgesehen wären. Rein technisch sehe ich auch nicht gerade etwas, was für UB spricht.

    Derived* und Base* sind völlig unverwandte Typen, und der Standard verlangt nicht dass sie gleich aussehen müssen.

    Angenommen Base besteht aus einen Byte, und die Maschine kann nur DWORDs addressieren. Und zwar 2^32 DWORDs. Ein Zeiger auf ein Byte braucht dann 2 DWORDs. Ein Zeiger auf etwas, was garantiert auf 4-Byte aligned ist, braucht aber nur ein DWORD. Derived könnte gross genug sein, um 4-Byte Alignment zu haben.

    sizeof(Base*) könnte also 8 sein, sizeof(Derived*) aber nur 4. Kaboom.

    Davon abgesehen...

    N3090, §5.2.10 - 6 schrieb:

    A function pointer can be explicitly converted to a function pointer of a different type. The effect of calling a function through a pointer to a function type (8.3.5) that is not the same as the type used in the definition of the function is undefined. Except that converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are function types) and back to its original type yields the original pointer value, the result of such a pointer conversion is unspecified.



  • hustbaer schrieb:

    sizeof(Base*) könnte also 8 sein, sizeof(Derived*) aber nur 4. Kaboom.

    Das wäre aber unlogisch, da Base* und Derived* definiert werden können, auch wenn Base und Derived unvollständig sind. Folglich müssen die Zeigergrössen gleich und unabhängig vom Pointee-Typen sein.

    Vielen Dank für den Standard-Auszug. Ist etwas schade, nun muss ich wohl tatsächlich zu std::function greifen und den Overhead in Kauf nehmen. Für diejenigen, die am Hintergrund interessiert sind: Es geht um eine Smart-Pointer-Klasse, welche den Deleter über einen Funktionszeiger speichert. Bei Derived-To-Base-Konvertierung des Smart-Pointers soll der Deleter natürlich gleich bleiben, aber die Signatur der Deleter-Membervariable ist eine andere.

    Nun ja, vielleicht ist es ohnehin eine gute Idee, flexiblere Funktionsobjekte zuzulassen. Mir gefällt halt der Gedanke nicht, dass meine Smart-Pointer-Objekte für die selten benötigte Flexibilität nun nicht mehr ganz so lightweight sind :xmas1:



  • Nexus schrieb:

    hustbaer schrieb:

    sizeof(Base*) könnte also 8 sein, sizeof(Derived*) aber nur 4. Kaboom.

    Das wäre aber unlogisch, da Base* und Derived* definiert werden können, auch wenn Base und Derived unvollständig sind. Folglich müssen die Zeigergrössen gleich und unabhängig vom Pointee-Typen sein.

    Da hast du jetzt auch wieder Recht. Hm. 🙂

    Vielen Dank für den Standard-Auszug. Ist etwas schade, nun muss ich wohl tatsächlich zu std::function greifen und den Overhead in Kauf nehmen. Für diejenigen, die am Hintergrund interessiert sind: Es geht um eine Smart-Pointer-Klasse, welche den Deleter über einen Funktionszeiger speichert. Bei Derived-To-Base-Konvertierung des Smart-Pointers soll der Deleter natürlich gleich bleiben, aber die Signatur der Deleter-Membervariable ist eine andere.

    Nun ja, vielleicht ist es ohnehin eine gute Idee, flexiblere Funktionsobjekte zuzulassen. Mir gefällt halt der Gedanke nicht, dass meine Smart-Pointer-Objekte für die selten benötigte Flexibilität nun nicht mehr ganz so lightweight sind :xmas1:

    Du kannst Type-Erasure verwenden.

    Wobei ich es gerade nicht mit weniger als 3 zusätzlichen Zeigern hinbekomme. D.h. dein Smart-Pointer wäre dann insgesamt 4 Zeiger gross (wenn er ausser dem Deleter und dem eigentlichen Zeiger nix speichert).

    Proof of concept:

    #include <iostream>
    
    template <class T>
    void MyPtrDeleterTrampoline(void* t, void (*anyD)(void*))
    {
    	typedef void DeleterFn(T*);
    	DeleterFn* d = reinterpret_cast<DeleterFn*>(anyD);	// cast back to original deleter type
    	d(static_cast<T*>(t));								// call original deleter with original argument type
    }
    
    class MyPtrDeleter
    {
    public:
    	typedef void AnyDeleterFn(void*);
    	typedef void DeleterTrampolineFn(void*, AnyDeleterFn*);
    
    	MyPtrDeleter()
    		: m_deleterFn(0),
    		m_deleterArg(0),
    		m_deleterFnTrampoline(0)
    	{
    	}
    
    	template <class T>
    	MyPtrDeleter(T* t, void (*d)(T*))
    		: m_deleterFn(reinterpret_cast<AnyDeleterFn*>(d)),
    		m_deleterArg(t),
    		m_deleterFnTrampoline(d ? &MyPtrDeleterTrampoline<T> : 0)
    	{
    	}
    
    	bool Armed() const
    	{
    		return m_deleterFnTrampoline != 0;
    	}
    
    	void Call()
    	{
    		if (Armed())
    			m_deleterFnTrampoline(m_deleterArg, m_deleterFn);
    	}
    
    	void Clear()
    	{
    		m_deleterFnTrampoline = 0;
    	}
    
    private:
    	AnyDeleterFn* m_deleterFn;
    	void* m_deleterArg;
    	DeleterTrampolineFn* m_deleterFnTrampoline;
    };
    
    template <class T>
    class MyPtr
    {
    public:
    	typedef void DeleterFn(T*);
    	explicit MyPtr(T* t, DeleterFn* d)
    		: m_deleter(t, d),
    		m_ptr(t)
    	{
    	}
    
    	MyPtr()
    		: m_ptr()
    	{
    	}
    
    	~MyPtr()
    	{
    		if (m_deleter.Armed())
    			std::cout << "~MyPtr<" << typeid(T).name() << ">...\n";
    		Reset();
    	}
    
    	template <class U>
    	void Steal(MyPtr<U>& other)
    	{
    		Reset();
    
    		m_deleter = other.m_deleter;
    		m_ptr = static_cast<T*>(other.m_ptr);
    
    		other.m_deleter.Clear();
    		other.m_ptr = 0;
    	}
    
    	T* Get()
    	{
    		return m_ptr;
    	}
    
    	void Reset()
    	{
    		m_deleter.Call();
    		m_deleter.Clear();
    		m_ptr = 0;
    	}
    
    private:
    	template <class U>
    	friend class MyPtr;		// one happy family
    
    	MyPtr(MyPtr const&);
    	MyPtr& operator = (MyPtr const&);
    
    	MyPtrDeleter m_deleter;
    	T* m_ptr;
    };
    
    struct Base1
    {
    	int b1;
    	Base1() : b1(1) {}
    };
    
    struct Base2
    {
    	int b2;
    	Base2() : b2(2) {}
    };
    
    struct Derived : public Base1, public Base2
    {
    	int d;
    	Derived() : d(3) {}
    };
    
    void DerivedDeleter(Derived* der)
    {
    	std::cout << "1 = " << der->b1 << "\n";
    	std::cout << "2 = " << der->b2 << "\n";
    	std::cout << "3 = " << der->d << "\n";
    	std::cout << "\n";
    	delete der;
    }
    
    int main()
    {
    	{
    		MyPtr<Derived> d(new Derived, &DerivedDeleter);
    	}
    
    	{
    		MyPtr<Derived> d(new Derived, &DerivedDeleter);
    		MyPtr<Base1> b1;
    		b1.Steal(d);
    	}
    
    	{
    		MyPtr<Derived> d(new Derived, &DerivedDeleter);
    		MyPtr<Base2> b2;
    		b2.Steal(d);
    	}
    
    	{
    		MyPtr<Derived> d(new Derived, &DerivedDeleter);
    		MyPtr<Base1> b1;
    		b1.Steal(d);
    		MyPtr<Derived> d2;
    		d2.Steal(b1);
    	}
    }
    


  • Vielen Dank. In den nächsten Tagen komme ich wohl nicht gross zum Testen, aber ich schau mir das sobald möglich genauer an.


Anmelden zum Antworten