Pointer auf operator delete[]



  • Hallo,

    ich habe eine Klasse der ich einen Zeiger auf eine Funktion übergebe, die zur Freigabe (und mehr) eines Member dient.

    template<class Class>
    class MyClass
    {
    	template<class Signature>
    	MyClass( 
    		Class* 	  Obj,
    		Signature DelFunc
    	)
    	{
    		// [...]
    	}
    };
    

    So, in der Regel sieht die Funktion wie folgt aus:
    void (__cdecl*)(void*)

    Da dies jedoch auch anders sein kann, muss ich auch diesen Fall handeln. Oben stehender Code funktioniert auch für alle möglichen Fälle:

    void Func1(void*);
    void Func2(Class*);
    Class* Func3(void*);
    Class* Func4(Class*);
    

    Dummerweiße jedoch nicht für den operator delete. Ich bekomme vom Compiler immer die Meldung:

    MyClass<Class>::MyClass : function does not take 2 arguments.
    

    Normalerweiße werden Objekte einer internen Datenbank hier gehandelt, im Zweifel muss jedoch einfach nur Delete aufgerufen werden. Wo liegt das Problem?



  • Wie rufst du denn delete auf?



  • Michael E. schrieb:

    Wie rufst du denn delete auf?

    Im Destruktor. I.d.R. wird nicht einfach der Operator delete, sondern eine umfangreichere delete Funktion aufgerufen:

    virtual ~MyClass()
    {
    	this->Delete(this->obj);
    }
    

  • Administrator

    Und was ist Delete für ein Objekt? Wie übergibst du den operator delete ?

    Ganz allgemein:
    Wieso machst du nicht noch einen Konstruktor, wo keine Delete-Funktion übergeben wird und das Delete Objekt auf nullptr oder sonst einen ungültigen Wert setzt? Im Destruktur prüfst du dann die Gültigkeit und falls es ungültig ist, löschst du es mit delete . Dürfte auch für den Anwender angenehmer sein, als dass er den operator delete übergeben muss.

    Grüssli



  • Das ganze sehe so aus. Den Operator kann man doch auch als default Parameter übergeben:

    template<class Class>
    class MyClass
    {
    private:
    	Class*	obj;
    	void (__cdecl *Delete)(Class*);
    
    public:
    	template<class Signature>
    	MyClass( 
    		Class* 	  Obj,
    		Signature DelFunc = ::operator delete
    	)
    	{
    		this->Delete = DelFunc;
    	}	
    
    	virtual ~MyClass()
    	{
    		this->Delete(this->obj);
    	}
    };
    

  • Administrator

    FrEEzE2046 schrieb:

    Das ganze sehe so aus. Den Operator kann man doch auch als default Parameter übergeben:

    Du verpasst damit aber die Möglichkeit, dass per delete auch standardmässig der operator delete der Klasse selbst aufgerufen wird, falls einer definiert ist 😉

    Zudem bin ich gerade nicht ganz sicher darüber, wie operator delete von der Standardbibliothek definiert sein muss. Auch gibt es mehrere davon. Mal schauen, ob da jemand anderes gerade noch zusätzliche Informationen auswendig kennt 🙂

    Grüssli



  • Zumindest bei MSDN ist nur die Rede von einer Überladung.

    Wo liegt denn grundsätzlich das Problem? Der Operator delete ist folgendermaßen forward deklariert:

    void operator delete(void*);
    

    Warum nimmt er diese Funktion nicht, void Func(void*) aber schon?


  • Administrator

    FrEEzE2046 schrieb:

    Zumindest bei MSDN ist nur die Rede von einer Überladung.

    Wo bitte soll dies im Artikel stehen? Im Standard steht es zudem anders.

    FrEEzE2046 schrieb:

    Wo liegt denn grundsätzlich das Problem? Der Operator delete ist folgendermaßen forward deklariert:

    Forward deklariert? Hast du dies selber gemacht? Eine Deklaration von operator delete ist implizit immer in jeder TU schon vorhanden!

    Wieso willst du diesen unnötigen, komplizierten und beschränkteren Weg gehen? Wie ich schon sagte, wenn du delete aufrufst, hast du mehr Standardverhalten, als wenn du probierst diese Funktion irgendwie zu speichern.

    Grüssli



  • Im Endeffekt möchte ich doch nur wissen, wo der große Unterschied zwischen

    void operator delete(void*) und
    void Func(void*)

    ist, da letzteres funktioniert und operator delete nicht. Mehr nicht. Ist doch legitim wisse zu wollen, woher der Compiler-Fehler kommt.
    Ich kann das ganze natürlich auch anders lösen, aber interessieren tut mich die Fehler-Ursache schon.

    Zudem weiß ich nicht, wie ich bei deiner Version dann explizit den operator delete oder den operator delete[] aufrufen soll.


  • Administrator

    FrEEzE2046 schrieb:

    ..., aber interessieren tut mich die Fehler-Ursache schon.

    Kannst du ein vollständig, kompilierbares und kleines Beispielprogramm zeigen, welches den Fehler reproduziert? Wenn diese Vorlesung weiterhin so stink langweilig ist, dann kann ich mir dies in den nächsten Stunden noch anschauen 😉

    Grüssli



  • Übrigens ist operator delete lediglich eine Speicherdeallokationsfunktion, es wird nicht der Destruktor aufgerufen. operator delete(x) ist nicht dasselbe wie delete x .



  • Okay, soweit verunstaltet, dass es fast wie eine SmartPointer Implementation ausschaut ;-). Naja, so ähnlich ist es ja auch fast.
    Hab's mal heruntergebrochen auf nen ganz normalen int Typen. Die Datenbankobjekte sagen euch sowieso nichts, da firmenintern.

    #include <iostream>
    
    using std::cout;
    
    template<class Class>
    class MyClass
    {
    private:
    	Class* obj;
    	void (__cdecl *const Delete)(Class*);
    
    public:
    	template<class Signature>
    	MyClass(Class* Obj, Signature DelFunc) : Delete((void (__cdecl*)(Class*))DelFunc), obj(Obj) {}	
    
    	virtual ~MyClass() {
    		this->Delete(this->obj);
    	}
    };
    
    void Delete1(void* memory)
    {
    	delete memory;
    }
    
    void Delete2(int* memory)
    {
    	delete memory;
    }
    
    int* Delete3(void* memory)
    {
    	delete memory;
    	return 0;
    }
    
    int* Delete4(int* memory)
    {
    	delete memory;
    	return 0;
    }
    
    int main()
    {
    	MyClass<int> myc1(new int, Delete1);
    	MyClass<int> myc2(new int, Delete2);
    	MyClass<int> myc3(new int, Delete3);
    	MyClass<int> myc4(new int, Delete4);
    
    	// Error	1	error C2660: 'MyClass<Class>::MyClass' : function does not take 2 arguments
    	MyClass<int> myc5(new int, operator delete);		
    
    	return 0;
    }
    

    Bashar schrieb:

    Übrigens ist operator delete lediglich eine Speicherdeallokationsfunktion, es wird nicht der Destruktor aufgerufen. operator delete(x) ist nicht dasselbe wie delete x .

    Ist hier auch nicht anders gewünscht. Aber mal was anderes: Was passiert eigentlich wenn man delete[] für ein "nicht-array" aufruft bzw. sollte man dann nicht immer delete[] aufrufen, wenn man nicht weiß, ob es sich um ein array handelt?



  • Undefiniertes Verhalten, was sonst 🙂



  • Bashar schrieb:

    Undefiniertes Verhalten, was sonst 🙂

    Ich habe mich damit noch nicht beschäftigt, aber delete[] muss ja irgendwie feststellen, wie viele Bytes es denn freigeben muss.


  • Administrator

    Professor wollte dann doch noch, dass wir nicht gelangweilt herumsitzen. Haben wohl zuviele in den Laptop getippt, statt aufgepasst 😃

    @FrEEzE2046,
    Was soll das bitte für ein Cast in deinem Konstruktor sein? Das ist ja sowas von abartig! Dadurch kann man wunderschönes undefiniertes Verhalten auslösen und der Kompiler winkt alles brav durch. Nimm diesen Cast da weg!
    Und nun wirst du sicher sagen: "Aber dann kommen Kompilerfehler!"
    So löst man aber keine Kompilerfehler und erst recht nicht diese. Ich empfehle dir, dass du mal boost::function , bzw. std::tr1::function anschaust. Das ist der bessere und saubere Weg.

    Wenn du das entsprechend korrigiert hast, wirst du übrigens auch sehen, dass der Kompiler meckert, weil operator delete ein überladener Operator ist. Genau wie ich es gedacht habe 😉

    FrEEzE2046 schrieb:

    ..., aber delete[] muss ja irgendwie feststellen, wie viele Bytes es denn freigeben muss.

    Wie delete[] den Speicher freigeben muss, gibt der Standard keine Vorgaben. Es ist schlicht und einfach kompilerabhängig. Und daher ist der Aufruf von delete[] , auf einen Speicherbereich der per new geholt wurde, undefiniert. Gleiches gilt im umgekehrten Fall ( delete und new[] ). Da bringt es nichts lange rumzuphilosophieren, wie ein Betriebsystem oder Kompiler dies nun tut. Sobald du sowas einsetzt, hast du undefiniertes Verhalten. Es mag auf diesem System mit diesem Kompiler funktionieren und beim nächsten Wechsel von einer dieser Variablen kracht das ganze Programm oder es gibt ein Speicherfehler, der aber dann wo völlig anders auftritt. Undefinierte Fehler zu beheben ist etwas vom mühsamsten, daher gleich erst gar nicht erlauben, dass solche Fehler auftreten könnten.

    Grüssli



  • Ich wollte eigentlich nicht extra eine Functor-Klasse bemühen, da dass aus meiner Sicht hier nicht nötig ist.

    Der Cast ist widerlich, ich weiß. Aber warum stellst dass ein so großes Problem dar?

    Im Endeffekt bleibt die Funktion doch immer wieder gleich. Wir haben:

    TypA Function(TypB);
    

    Das bringe ich immer in die Form:

    void Function(Class*);
    

    So. Da in den Klammern immer ein Pointer und damit entsprechend der Wortbreite des Professors, viele Byte auf den Stack gepushed werden sehe ich hier keinen Grund für undefiniertes Verhalten. Das bestätigt auch das Assembly-Listing.

    Schlussendlich ist auf unterster Ebene doch egal, um was für einen "Typ" es sich hier handelt, so lange die Größe gleich bleibt.

    Jede Funktion liegt an einer speziellen Adresse die der Kompiler kennen muss. Er weiß nun auch wie viele Byte er auf den Stack zu pushen hat. Und dass der Rückgabewert nun void ist ... na und? Dass bedeutet nur, dass ich den Wert aus dem eax Register nicht als return value benutze.

    Ich sehe da nicht wirklich das Problem.



  • FrEEzE2046 schrieb:

    Ich wollte eigentlich nicht extra eine Functor-Klasse bemühen, da dass aus meiner Sicht hier nicht nötig ist.

    Der Cast ist widerlich, ich weiß. Aber warum stellst dass ein so großes Problem dar?

    Sorry wenn ich grad mal völlig off topic bin, aber hier stellt was anderes ein großes Problem dar: http://www.dass-das.de/


  • Administrator

    Bisschen undefiniertes Verhalten gefälligst?

    template<class Class> 
    class MyClass 
    { 
    private: 
        Class* obj; 
        void (__cdecl *const Delete)(Class*); 
    
    public: 
        template<class Signature> 
        MyClass(Class* Obj, Signature DelFunc) : Delete((void (__cdecl*)(Class*))DelFunc), obj(Obj) {}    
    
        virtual ~MyClass() { 
            this->Delete(this->obj); 
        } 
    }; 
    
    void foo(short, char, int)
    {
    }
    
    void bar(MyClass<int>, MyClass<void>)
    {
    }
    
    int main() 
    {
        // Mal schauen ...
        MyClass<int> myc1(new int, &foo); 
        MyClass<int> myc2(new int, &bar);
    
        // Oder vielleicht?
        MyClass<int> myc3(new int, 'c'); 
        MyClass<int> myc4(new int, "error");
        MyClass<int> myc5(new int, 2343577); 
    
        // Und solchen Zucker kann man auch erzeugen.
        MyClass<int> myc6(new int, &myc1);
    
        return 0; 
    }
    

    Es kompiliert, man kann es ausführen, allerdings besteht die Möglichkeit, dass man damit den Weltuntergang heraufbeschwört 😃

    Du umgehst eben jegliche Typsicherheit. Das ist ja auch ein Grund, wieso man C Cast oder auch reinterpret_cast vermeiden sollte.

    Grüssli



  • ah,
    jetzt verstehen wir uns. Wenn ich natürlich gar keine Funktion, sondern irgengwas anderes, übergebe kommt natürlich irgendetwas undefiniertes heraus.

    Lässt sich aber eigentlich leicht umgehen:

    template<class Signature>
    class IsFunction;
    
    template<typename R, typename P0>
    class IsFunction<R (__cdecl*)(P0)> {};
    
    // [...]
    
    // error C2079: 'MyClass' uses undefined class
    MyClass<int> myc5(new int, "error");
    

    EDIT:
    Natürlich kann man auch noch prüfen, ob es sich wirklich um einen Pointer handelt:

    // zur Laufzeit:
    
    template<class Signature>
    struct FunctionTypeExtractor;
    
    template<typename R, typename P0>
    struct FunctionTypeExtractor<R (__cdecl*)(P0)> 
    {
    	enum {ParamSize = sizeof(P0)};
    };
    
    template<class Class>
    class MyClass
    {
    public:
    	template<class Signature>
    	MyClass(Class* Obj, Signature DelFunc) : obj(Obj) 
    	{
    		if( FunctionTypeExtractor<Signature>::ParamSize == sizeof(void*) )
    		{
    			this->Delete = (void (__cdecl*)(Class*))DelFunc;
    		}
    		else
    			throw std::exception("error"); 
    	}	
    };
    
    // Zur Kompilierzeit:
    
    template<unsigned n>
    class SIZE_OF_POINTER;
    
    template<>
    class SIZE_OF_POINTER<sizeof(void*)> {};
    
    template<class Signature>
    struct FunctionTypeExtractor;
    
    template<typename R, typename P0>
    struct FunctionTypeExtractor<R (__cdecl*)(P0)> 
    {
    	SIZE_OF_POINTER<sizeof(P0)> dummy;
    };
    
    template<class Class>
    class MyClass
    {
    public:
    	template<class Signature>
    	MyClass(Class* Obj, Signature DelFunc) : obj(Obj) 
    	{
    		FunctionTypeExtractor<Signature> dummy;
    		this->Delete = (void (__cdecl*)(Class*))DelFunc;
    	}	
    };
    

  • Administrator

    @FrEEzE2046,
    Und du bist immer noch der Meinung, dass du keine "Funktor-Klasse" benötigst? Du zerstörst die Typsicherheit und probierst sie dann irgendwie halb über Templatemataprogrammierung wieder einzuführen. Sogar teilweise über Laufzeitfehler statt Kompilerfehler und ich hoffe doch, dass du mit mir übereinstimmst, dass Kompilerfehler viel besser sind als Laufzeitfehler. All das zeigt doch schon, dass da etwas nicht stimmt.

    Zudem auch mit deinen bisherigen Sicherheiten, kann man immer noch Unsinn bauen:

    void foo(int) {}
    void bar(char*) {}
    
    // ...
    
    MyClass<int> a(new int, &foo);
    MyClass<int> b(new int, &bar);
    

    Unsinn bedeutet hier, dass man versehentlich falsche Funktion übergeben könnte. Durch einen Schreibfehler, eine Unachtsamkeit oder sonstwas. Was das am Ende heissen wird, ist völlig frei. Womöglich löschst du tatsächlich die Datenbank mit so einem Fehler, weil durch den Schreibfehler eine Funktion zum löschen der Datenbank aufgerufen wurde und zufälligerweise der richtige Parameter übergeben wurde.

    Und nicht zuletzt zeigt auch dein Kompilerfehler-Problem, wegen welchem du diesen Thread eröffnet hast, dass dein Cast irgendwelchen Unsinn produziert. Als Templateparameter kann nämlich eine überladene Funktion aktzeptiert werden, nur kann man sie halt nicht in einem solchen Funktionszeiger ablegen. Der Kompiler ist dann anscheinend nicht mal mehr in der Lage eine sinnvolle Fehlermeldung zu generieren, bei einem solch dreckigen Cast 😉

    Was man auch nicht vergessen sollte, bzw. was auch noch ein grosser Vorteil von z.B. boost::function , bzw. std::tr1::function , ist, dass man auch Funktoren übergeben kann. Man hat eine deutlich grössere Freiheit und trotzdem volle Typsicherheit.

    Braucht es noch mehr, um dich von diesem falschen Weg abzubringen? Oder willst du wirklich lieber ins Verderben rennen? 😃

    Grüssli


Log in to reply