new- und delete-operator überschreiben



  • @Helmut-Jakoby
    Wenn dein Compiler das Makro __PRETTY_FUNCTION__ unterstützt würde ich das Parameter für deine newÜberladung benutzen. Ist ne Quality of Life Funktion, die das Auswerten der Log einfacher macht, weil man sofort sieht, in welcher Funktion der new Aufruf passiert ist. Ich habe sowas bei mir in einem TRACE Makro und mag nicht mehr ohne 😉

    #ifdef __PRETTY_FUNCTION__
     #define new new(__FILE__, __PRETTY_FUNCTION__, __LINE__ )
    #else
     #define new new(__FILE__, __LINE__)
    #endif


  • Man könnte auch CaptureStackBackTrace verwenden und die "normalen" operator new Funktionen ersetzen. Damit bekommt man mehr & bessere Informationen. Natürlich müsste man den dann zur Anzeige noch auflösen, aber dafür gibt's ja die dbghelp Funktionen.



  • __PRETTY_FUNCTION__ ist eine gcc-Erweiterung.

    Seit C99 bzw. C++11 gibt es __func__ - dies wird auch vom VS (bzw. MSVC) unterstützt, s.a. Predefined macros (sowie weitere __FUNC...__-Makros).



  • Hallo an alle Antwortenden,
    lieben herzliche Dank für Eure interessanten Beiträge!
    Das mit __PRETTY_FUNCTION__ werde ich beizeiten mit dem GCC ausprobieren.
    Ansonsten pausiere ich jetzt erstmal bis Januar 22.
    Werde mich dann weiter mit dem überladen vom new- und delete-operator beschäftigen und mich an dieser Stelle wieder melden.
    Ich wünsche Euch allen schöne Feiertage und einen angenehmen Übergang ins neue Jahr.
    Helmut



  • @Th69 sagte in new- und delete-operator überschreiben:

    @john: Schau mal in Zeile 25 des Codes vom Eingangsbeitrag!!!

    • Das Makro lässt sich nicht mit g++ übersetzen.
    • Das Makro bricht gültigen C++ Code!

    Nun, hatte ich versucht die Routine inline zu deklarieren, aber der nvc++ gibt dazu folgende Warnung heraus.

    "new.h", line 9: warning: declaring a new or delete operator "inline" is nonstandard
      inline void* operator new (std::size_t s) {
      ^
    
    "new.h", line 25: warning: declaring a new or delete operator "inline" is nonstandard
      inline void operator delete (void* p) {
      ^
    

    Ok, das ist relativ eindeutig. Also den Code nochmals angepasst und in Header und Implementation aufgetrennt.

    Es bleibt halt die Frage was man so einer Übung erreichen will. Es gibt üblicherweise Tools, mit denen man die Memory Leaks leicht finden kann, da braucht man da nicht selbst herumzuspielen und versuchen das Rad neu erfinden zu wollen.

    // new.h
    #ifndef NEW_H
    #define NEW_H
    
    #include <new>
    #include <cstddef>
    #include <cstdlib>
    #include <iostream>
    
    void* operator new (std::size_t s);
    void* debug_new (std::size_t s, const char* const str);
    void* operator new (std::size_t s, const char* const str);
    void operator delete (void* p);
    // nur um den Compiler Warnings zu unterdrücken
    void operator delete (void* p, std::size_t s [[maybe_unused]]);
    
    #ifdef DEBUG 
    #define NEW new(__FILE__)
    #elif defined NEW_NO_COMPILE
    #define new new(__FILE__)
    #else
    #define NEW new
    #endif
    
    #endif
    
    // new.cc
    #include "new.h"
    
    void* operator new (std::size_t s) {
        std::cout << "überschriebenes new\n";
    
        return malloc(s);
    }
    
    void* operator new (std::size_t s, const char* const str) {
        return debug_new(s, str);
    }
    
    void* debug_new (std::size_t s, const char* const str) {
        std::cout << "debug_new " << str << "\n";
        return malloc(s);
    }
    
    void operator delete (void* p) {
        std::cout << "überschriebenes delete\n";
    
        free(p);
    }
    
    // nur um den Compiler Warnings zu unterdrücken
    void operator delete (void* p, std::size_t s [[maybe_unused]]) {
        std::cout << "delete Datei: " << __FILE__ << ", Zeile " << __LINE__ << "\n";
    
        free(p);
    }
    
    // class.h
    #ifndef CLASS_H
    #define CLASS_H
    
    #include <new>
    #include <cstddef>
    #include <iostream>
    
    class C {
    public:
        void f();
    
        static void* operator new (std::size_t s);
        static void operator delete (void* p);
    };
    #endif
    
    // class.cc
    #include "class.h"
    // Die Klasse C definiert aus einem nicht näher bestimmten Grund einen eigenen operator new.
    // Wenn man das braucht, gibt es die seit langem die Empfehlung das nicht global zu tun, sondern
    // in einer Klasse selbst zu machen – siehe z.B. Effective C++ von Scott Meyers.
    // Da wir das Rad nicht neu erfinden wollen, nutzen wir hier den globalen operator new, mit
    // weiterem Code der hier als Beispiel nur ausgibt, dass dieses Routine aufgerufen wurde.
    
    void* C::operator new (std::size_t s) {
        std::cout << "C::new\n";
    
        return ::operator new(s);
    }
    
    void C::operator delete (void* p) {
        std::cout << "C::delete\n";
    
        ::operator delete(p);
    }
    
    void C::f () {
        std::cout << "C::f()\n";
    }
    
    // main.cc
    
    #include <iostream>
    // Wir wollen ja auch die Speicheranforderungen der eigenen Klassen verfolgen.
    // Deshalb wird new.h vor class.h inkludiert, sonst funktioniert das Verfolgen
    // der eigenen Klassen nicht.
    #include "new.h"
    #include "class.h"
    
    int main () {
    #ifdef DEBUG 
        int* p = NEW int;
        C* q = NEW C;
    #else
        int* p = new int;
        C* q = new C;
    #endif
    
        delete q;
        delete p;
    }
    
    LD=g++
    CCC=g++
    CCFLAGS=-std=c++17 -Wpedantic -Wall -Wextra -DNEW_NO_COMPILE
    LDFLAGS=
    
    TARGETS=main
    OBJS=main.o class.o new.o
    
    .PHONY: all clean
    .SUFFIXES: .cc
    all: $(TARGETS)
    
    clean:
        rm -f $(OBJS) $(TARGETS)
    
    main: $(OBJS)
        $(LD) $(LDFLAGS) $(OBJS) -o main
    
    .cc.o:
        $(CCC) $(CCFLAGS) -c $< -o $@
    

    Nachtrag: Die Dateien fürs Beispiel angepasst, um den Fehler besser hervor zu heben, Makefile hinzugefügt und Kommentare füin den Sourcen hinzugefügt.



  • @john-0 sagte in new- und delete-operator überschreiben:

    Nun, hatte ich versucht die Routine inline zu deklarieren, aber der nvc++ gibt dazu folgende Warnung heraus.

    Das ist äußerst nett vom Compiler, da "no diagnostic required":

    The program is ill-formed, no diagnostic required if more than one replacement is provided in the program for any of the replaceable allocation function, or if a replacement is declared with the inline specifier.
    https://en.cppreference.com/w/cpp/memory/new/operator_new

    #define new new(__FILE__)
    

    Die sauberste Lösung um die Stelle der Allokation zu finden dürfte wohl tatsächlich ein Backtrace sein. Entweder mit dem von @hustbaer erwähnten CaptureStackBackTrace und für GCC/Clang sollte das eigentlich mit libbacktrace zu machen sein, wenn ich das richtig sehe. Zusammen mit Debug-Informationen könnte so ne Lösung auch Quellcodedatei und Zeilenummer ausspucken. Die Macro-Lösung ist vor allem deshalb holprig, weil sie nur dort funktioniert, wo man diesen Code auch eingebunden und kompiliert hat. Da dürfte es gerade in Kombination mit Bibliotheken (z.B. auch der Standardbibliothek) eine Menge Fälle geben, wo das nicht auf Anhieb oder nur nach fummeligen Anpassungen funktioniert.

    Es bleibt halt die Frage was man so einer Übung erreichen will. Es gibt üblicherweise Tools, mit denen man die Memory Leaks leicht finden kann, da braucht man da nicht selbst herumzuspielen und versuchen das Rad neu erfinden zu wollen.

    Und ja, genau das, wenn man keinen guten Grund hat, es selbst zu implementieren. Gibt sicher auch Bibliotheken für nen gutes Malloc-Debugging, wenns in das Programm integriert werden soll statt einem externen Tool (hatte mir vor Zeiten mal ein paar alternative Malloc-Implementierungen angesehen, da gibts schon welche mit praktischen Debugging-Features, ist aber schon was her). Da sollte man dann auch deutlich bessere Informationen rausholen können so á la "ich hab hier so nen Pointer, sag mir mal wo der Speicher dazu eigentlich herkommt".



  • @john-0 sagte in new- und delete-operator überschreiben:

    • Das Makro lässt sich nicht mit g++ übersetzen.
    • Das Makro bricht gültigen C++ Code!

    Warum nicht? Ideone-Code C++ (gcc 8.3) sowie Ideone-Code C++14 (gcc 8.3) funktioniert.
    Dort kann ich zwar nicht die Aufteilung in Header und Implementierung zeigen, wichtig ist jedoch nur, daß der new.h-Header in den Source-Dateien als letztes eingebunden wird (und nicht z.B. das Makro in einer der Standardheader, wie <new> aufgelöst wird).

    Sicherlich gibt es andere Lösungen um Speicherlecks (im eigenen Programmcode) zu finden, aber dies ist eben eine (relativ einfache) mögliche Lösung.



  • Allen ein frohes und besinnliches Weihnachtsfest

    @Th69 sagte in new- und delete-operator überschreiben:

    @john-0 sagte in new- und delete-operator überschreiben:

    • Das Makro lässt sich nicht mit g++ übersetzen.
    • Das Makro bricht gültigen C++ Code!

    Warum nicht? Ideone-Code C++ (gcc 8.3) sowie Ideone-Code C++14 (gcc 8.3) funktioniert.

    Ich habe die Sourcen nochmals angepasst. Wenn man bestimmte Dinge unterlässt funktioniert es. Aber es besteht die Gefahr, dass man das ganz schnell fehlschlägt und gültigen Code nicht mehr übersetzen lässt. Die Fehlermeldung mit den aktualisierten Sourcen sieht nun so aus.
    Die Fehlermeldung von GCC 9.3.0 (-DNEW_NO_COMPILE fürs Übersetzen nutzen)

    g++ -std=c++17 -Wpedantic -Wall -Wextra -DNEW_NO_COMPILE -c main.cc -o main.o
    In file included from main.cc:2:
    new.h:20:16: error: declaration of ‘operator new’ as non-function
       20 | #define new new(__FILE__)
          |                ^
    class.h:12:24: note: in expansion of macro ‘new’
       12 |  static void* operator new (std::size_t s);
          |                        ^~~
    new.h:20:13: error: expected ‘;’ at end of member declaration
       20 | #define new new(__FILE__)
          |             ^~~
    class.h:12:24: note: in expansion of macro ‘new’
       12 |  static void* operator new (std::size_t s);
          |                        ^~~
    new.h:20:17: error: expected unqualified-id before string constant
       20 | #define new new(__FILE__)
          |                 ^~~~~~~~
    class.h:12:24: note: in expansion of macro ‘new’
       12 |  static void* operator new (std::size_t s);
          |                        ^~~
    new.h:20:17: error: expected ‘)’ before string constant
       20 | #define new new(__FILE__)
          |                 ^~~~~~~~
    class.h:12:24: note: in expansion of macro ‘new’
       12 |  static void* operator new (std::size_t s);
          |                        ^~~
    new.h:20:16: note: to match this ‘(’
       20 | #define new new(__FILE__)
          |                ^
    class.h:12:24: note: in expansion of macro ‘new’
       12 |  static void* operator new (std::size_t s);
          |                        ^~~
    make: *** [makefile:20: main.o] Fehler 1
    
    

    Dort kann ich zwar nicht die Aufteilung in Header und Implementierung zeigen, wichtig ist jedoch nur, daß der new.h-Header in den Source-Dateien als letztes eingebunden wird (und nicht z.B. das Makro in einer der Standardheader, wie <new> aufgelöst wird).

    Das nützt Dir aber auch nichts, weil der Aufruf des operator new der klassenspezifisch ist und dann trotzdem falsch ist, zu dem würdest Du dann von diesen Klassen die Speicheranforderung nicht mehr verfolgen. Das wäre dann wenig sinnvoll.



  • Du darfst natürlich nicht den Header "new.h" vor der Definition der Operatoren einbinden, sondern nur bei der Benutzung von new zur Speicheranforderung (also auch nicht in new.cc - oder explizit wieder #undef new danach schreiben)...

    Und klassenspezifische new- und delete-Operatoren sind ja auch sehr speziell (ich habe dies in Anwendercode noch nie gesehen).
    Und dann könnte man ja eine eigene Funktion zur Verfügung stellen dafür, welche das Speichern in der Map übernimmt (wenn denn unbedingt gewünscht).

    Edit: In der Top-Antwort von Macro to replace C++ operator new gibt es noch eine Variante mittels zweier globaler Variablen und dem Überschreiben des Standard new-Operators.



  • Zitat @john-0 :"Es bleibt halt die Frage was man so einer Übung erreichen will."

    Ich sag mal, "Erkenntnis durch Handeln" nach John Deweys und ich will evtl. kein Tool nutzen, was mir gefühlte 1000 Informationen liefert, die ich lernen muss zu verstehen.
    Alles gute fürs neue Jahr!
    Helmut



  • Da mich das Makro Gewurschtel gestört hat, habe ich noch einmal die Sache genauer angesehen, und habe da eine C++ saubere Lösung umgesetzt, die dann allerdings g++ abhängig ist (unter Linux läuft auch mit einigen anderen Compilern). Es wird der Funktionsname und der Offset ausgegeben, in dem die Backtrace Option der Laufzeitumgebung genutzt wird. Es ist nicht ganz so schön wie die Ausgabe der Name der Sourcedatei und der Zeilennummer, aber dafür funktioniert es auch mit anderem Code.

    Nachtrag: Das wichtigste nicht angeführt, man muss die Linker Option "-rdynamic" nutzen.

    // new.h
    #ifndef NEW_H
    #define NEW_H
    
    #include <new>
    #include <cstddef>
    #include <cstdlib>
    #include <iostream>
    
    void* operator new (std::size_t s);
    void* operator new[] (std::size_t s);
    void operator delete (void* p);
    void operator delete (void* p, std::size_t s [[maybe_unused]]);
    void operator delete[] (void* p);
    void operator delete[] (void* p, std::size_t s [[maybe_unused]]);
    
    #endif
    
    // new.cc
    #include "new.h"
    #include <execinfo.h>
    #include <cxxabi.h>
    #include <cstring>
    #include <ostream>
    
    static constexpr size_t size = 1024;
    
    class VBuffer {
    public:
    	void** p;
    	VBuffer (size_t s) : p (static_cast<void**>(malloc(s*sizeof(s)))) {}
    	~VBuffer () {free(p);}
    	// todo rule of five
    };
    
    class Buffer {
    public:
    	char* p;
    	size_t s_;
    	size_t offset;
    	Buffer(size_t s) : p (static_cast<char*>(malloc(s))), s_(s) {}
    	~Buffer() {free(p);}
    	// todo rule of five
    };
    
    std::ostream&
    operator<< (std::ostream& o, const Buffer& b) {
    	o << "called from \"" << b.p << "\" with offset " << b.offset;
    	return o;
    }
    
    void get_function_name (Buffer& name) {
    	int size = name.s_;
    	VBuffer buffer(name.s_);
    	Buffer  demangle(name.s_);
    	size_t ss = name.s_;
    
    	int    depth = backtrace (buffer.p, size);
    	char** strs  = backtrace_symbols (buffer.p, depth);
    
    	char* str = strs[2];
    	char* start = strchr(str, '(') + 1;
    	char* end = strchr(str, '+');
    	auto len = end - start;
    
    	sscanf (end+1, "%zx", &name.offset);
    
    	strncpy (name.p, start, len);
    	name.p[len] = '\0';
    	int status = 0;
    
    	char* demangledName = abi::__cxa_demangle (name.p, demangle.p, &ss, &status);
    	if (0 == status) {
    		strncpy (name.p, demangledName, ss);
    		name.p[ss] = '\0';
    	} 
    
    	free(strs);
    }
    
    void* operator new (std::size_t s) {
    	Buffer name(size);
    	get_function_name(name);
    	std::cout << "overloaded new " << name << "\n";
    	return malloc(s);
    }
    
    void* operator new[] (std::size_t s) {
    	Buffer name(size);
    	get_function_name(name);
    	std::cout << "overloaded new[] " << name << "\n";
    	return malloc(s);
    }
    
    void operator delete (void* p) {
    	Buffer name(size);
    	get_function_name(name);
    	std::cout << "delete(void* p) " << name << "\n";
    	free(p);
    }
    
    void operator delete (void* p, std::size_t s [[maybe_unused]]) {
    	Buffer name(size);
    	get_function_name(name);
    	std::cout << "delete(void* p, size_t s) " << name << "\n";
    	free(p);
    }
    
    void operator delete[] (void* p) {
    	Buffer name(size);
    	get_function_name(name);
    	std::cout << "delete[] (void* p) " << name << "\n";
    	free(p);
    }
    
    void operator delete[] (void* p, size_t s [[maybe_unused]]) {
    	Buffer name(size);
    	get_function_name(name);
    	std::cout << "delete[] (void* p, std::size_t s) " << name << "\n";
    	free(p);
    }
    


  • So, habe zumindest für Visual Studio fertig (mit mingw und gcc habe ich aufgegeben).

    Speicher-Leaks werden (auch in eigenen DLLs) gefunden und mit Dateinamen und Zeilennummer in einer std::unordered_map gespeichert. Funktioniert durch Verwendung eines mutex auch in Threads.

    Die Leaks können, wenn vorhanden, vor Beendigung des Programms wahlweise in der Konsole oder Ausgabe von Visual Studio ausgegeben werden.

    Das ganze funktioniert auch im Release-Modus und ganz ohne MFC.

    Wenn noch jemand Interesse hat; http://globalobjects.de/memoryleakindicatorproject_de.html


Anmelden zum Antworten