Forum: Compiler & IDEs cpp memory leaks vermeiden


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Christoph M. (mchris)


Lesenswert?

Auf einem Mikrocontroller will ich einem Array mehrere Objekte anlegen. 
Die sollen dann mit einer "exec" Funktion benutzt werden. Später will 
ich diese dann löschen und wieder neu anlegen. Wie verhindere ich ein 
Speicherleck?

Hier mal ein Beispiel für die Objekte:
1
#include <iostream>
2
3
class Counter {
4
private:
5
    int count;
6
7
public:
8
    Counter() : count(0) {}  // Constructor initializes count to 0
9
10
    void exec() {
11
        count++;
12
    }
13
14
    int getCount() const {
15
        return count;
16
    }
17
};
18
19
class Counter_IF {
20
private:
21
    Counter counterInstance;  // Instance of Counter
22
23
public:
24
    void exec() {
25
        counterInstance.exec();  // Call exec on the Counter instance
26
    }
27
28
    int getCount() const {
29
        return counterInstance.getCount();  // Retrieve count from the Counter instance
30
    }
31
};
32
33
int main() {
34
    Counter_IF counters[5];  // Array of 5 Counter_IF instances
35
36
    // Call exec on some of the counters
37
    counters[0].exec();
38
    counters[1].exec();
39
    counters[1].exec();
40
    counters[2].exec();
41
    counters[2].exec();
42
    counters[2].exec();
43
44
    // Display the counts of all counters
45
    for (int i = 0; i < 5; i++) {
46
        std::cout << "Counter " << i << " count: " << counters[i].getCount() << std::endl;
47
    }
48
49
    return 0;
50
}

von Michael B. (laberkopp)


Lesenswert?

Christoph M. schrieb:
> Wie verhindere ich ein Speicherleck?

So bald main verlassen wird, wird Counter_IF aufgelöst.

Neben dem statisch dimensionierten Array könnte das auch ein 
indizierbares Klassenobjekt sein.

von Christoph M. (mchris)


Lesenswert?

>So bald main verlassen wird, wird Counter_IF aufgelöst.

Danke. Mein Beispiel war etwas irreführend. Hier die Main noch mal neu.
Die Liste für die Counter_IF ist 10 Einträge lang.
Zuerst werden 5 Counter erzeugt und dann laufen lassen.
In der zweiten Runde werden 8 Counter erzeugt. Die 5 vorher benutzen 
werden eigentlich nicht mehr benötigt,aber ich schätze, sie hängen immer 
noch im Speicher herum. Wie kann man das feststellen?
1
#define COUNTERLIST_LEN 10
2
int main() {
3
4
    Counter_IF *Liste[COUNTERLIST_LEN];
5
6
    int idx=0;
7
8
    // Zaeher anlegen
9
    int numCounters=5;
10
    for(idx=0;idx<numCounters;idx++) Liste[idx] = new Counter_IF();
11
12
    // Zaehler laufen lassen
13
    for(idx=0;idx<numCounters;idx++) Liste[idx]->exec();
14
15
    // Zaeher new anlegen
16
    numCounters=8;
17
    for(idx=0;idx<numCounters;idx++) Liste[idx] = new Counter_IF();
18
19
    // Zaehler laufen lassen
20
    for(idx=0;idx<numCounters;idx++) Liste[idx]->exec();
21
22
    // Anzeigen
23
    for(idx=0;idx<numCounters;idx++)
24
    {
25
        std::cout << "Counter " << idx << " count: " << Liste[idx]->getCount() << std::endl;
26
    }
27
28
    return 0;
29
}

von Christoph M. (mchris)


Lesenswert?

Um das genauer zu untersuchen, brauch man wohl erst einmal einen 
"Speicherverbrauchsmesser".
Hier mein Versuch mit einem Arduino Nano.

Seltsamerweise scheint das Anlegen des Buffers keinen Speicher zu 
brauchen. Es sollten ja 10 Byte mehr sein ..

Die Frage ist: sind die Funktionen für die "Verbrauchsmessung" geeignet?

Free RAM: 1850
heapStartMemAddress: 1577
lastHeapMemAddress: 0
Free RAM: 1850
1
#include <stdlib.h>
2
3
/*
4
 Memory allocation: __brkval points to the last memory address used by the heap. 
5
 When dynamic memory is allocated (e.g., using malloc()),
6
  __brkval is adjusted to reflect the new heap boundary.
7
*/
8
int lastHeapMemAddress()
9
{
10
  extern int *__brkval;
11
  return (int) __brkval;
12
}
13
int heapStartMemAddress()
14
{
15
  extern int *__heap_start;
16
  return (int) __heap_start;
17
}
18
19
int freeRam() {
20
  
21
/*
22
    It checks if __brkval is 0.
23
    If __brkval is 0 (meaning no heap allocations have been made yet), it calculates the free memory from &v to &__heap_start.
24
    If __brkval is not 0, it calculates the free memory from &v to __brkval.
25
    Finally, it returns the calculated free memory.
26
*/
27
  extern int __heap_start, *__brkval;
28
  int v;
29
  int free_memory;
30
31
  if (__brkval == 0) {
32
    free_memory = (int) &v - (int) &__heap_start;
33
  } else {
34
    free_memory = (int) &v - (int) __brkval;
35
  }
36
37
  return free_memory;
38
}
39
40
41
void setup() {
42
  Serial.begin(115200);
43
  Serial.println();
44
  Serial.print(F("Free RAM: "));  Serial.println(freeRam());
45
  Serial.print(F("heapStartMemAddress: "));  Serial.println(heapStartMemAddress());
46
  
47
  uint8_t buffer[3]={0};
48
  
49
  Serial.print(F("lastHeapMemAddress: "));  Serial.println(lastHeapMemAddress());
50
  Serial.print(F("Free RAM: "));  Serial.println(freeRam());
51
  for(int n=0;n<10;n++)Serial.println(buffer[n],HEX);
52
53
}
54
55
void loop() {
56
  // Your code here
57
}

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> Die 5 vorher benutzen werden eigentlich nicht mehr benötigt,aber ich
> schätze, sie hängen immer noch im Speicher herum.

Brauchst du den Speicher jemals für was anderes? Spricht was dagegen 
einfach fix 10 Instanzen zu erzeugen und einfach nur die jeweils 
benötigte Anzahl zu benutzen?

Wenn man wirklich dynamisch allokiert (mit new/malloc) muss man auch 
wieder freigeben (delete/free). Aber das ist auf Mikrocontrollern selten 
sinnvoll. Wenn dann sollte man zB std::unique_ptr nutzen, was dieses 
sicherer kapseln. Leider steht das auf dem AVR-GCC nicht zur Verfügung.

Christoph M. schrieb:
> Um das genauer zu untersuchen, brauch man wohl erst einmal einen
> "Speicherverbrauchsmesser".

Das ist eigentlich selten nötig. SBRK wird eigentlich nie reduziert, 
wozu auch, es gibt ja keine anderen Programme die den Speicher dann 
bekommen könnten. Du kannst nicht so einfach die aktuelle 
Speichernutzung abfragen, die zeichnet C/C++ nicht explizit auf, weil 
ineffizient.

Dein Buffer liegt auf dem Stack, der wird natürlich von SBRK überhaupt 
nicht erfasst. Den Stack kann man zwar auch abfragen, aber muss das 
sein...

Gerade auf kleinen Mikrocontrollern ist dynamische Speicherverwaltung 
selten sinnvoll!

von Michael B. (laberkopp)


Lesenswert?

Christoph M. schrieb:
> Mein Beispiel war etwas irreführend

Christoph M. schrieb:
> new

Wer new sagt muss auch delete aufrufen. Und damit das immer passiert, 
nutzt man eine Klasse zur Arrayverwaltung und den Destruktor darin.

Das sind aber Grundlagen der objektorientierten Programmierung,

wegen der Freigabeproblematik in Standard-C insbesondere bei exceptions 
(oder poor mans exception: ljmp) hat man ja Klassen mit Destruktoren als 
Erweiterung der reinen Datenstrukturen struct überhaupt erst eingeführt.

von J. S. (jojos)


Lesenswert?

Ein delete ist nicht Pflicht, wenn man Objekte eben Pseudo-statisch 
anlegt. Das macht auch auf μC Sinn, z.B. wenn man eine 
Konfigurationsdatei einliest und anhand derer Objekte anlegt. Dann muss 
man natürlich auswerten ob das Erfolgreich war und Fehler anzeigen 
können.

Bei dem Counter Beispiel könnte man auch ein used flag  verwenden. 
Erstmal suchen ob einer frei ist und den dann benutzen, wenn nicht neuen 
anlegen. Wird auch billiger sein als delete.

: Bearbeitet durch User
von Andras H. (andras_h)


Lesenswert?

Wie man einen Speicherleck vermeidet?
 - Requirements schreiben + Review machen
 - Architektur aufmalen + Review machen
 - Design von Modulen machen + Review machen
 - Implementieren des Designs + Review machen, MISRA plus es gibt andere 
statische und dynamische code checks.
 - Unbedingt tests schreiben. Unit oder Module test, wie man es nennen 
mag. Dann noch integration und system tests auch.

Wenn man das alles gemacht hat, dann sind vermutlich alle Speicherlecks 
vermieden. Wenn da doch eins entsteht, dann muss man gucken wo, wieso, 
weshalb. Und auf den Fall den Prozess erweitern.

von Oliver S. (oliverso)


Lesenswert?

Andras H. schrieb:
> Wie man einen Speicherleck vermeidet?

Kein new verwendet. C++ hat Container, die übernehmen das. Wenn's 
wirklich keine vollständige C++-stdlib gibt, das ganze Konzept 
überdenken.

Oliver

von Christoph M. (mchris)


Lesenswert?

Niklas G. (erlkoenig)
>Brauchst du den Speicher jemals für was anderes? Spricht was dagegen
>einfach fix 10 Instanzen zu erzeugen und einfach nur die jeweils
>benötigte Anzahl zu benutzen?

Fixe Instanzen geht leider nicht.

Andras H. (andras_h)
> Wie man einen Speicherleck vermeidet?
> - Requirements schreiben + Review machen

Geil. Ich wollte ja schon immer "Requirements" schreiben. Das hilft ja 
bekanntlich bei jeder Entwicklung, wie konnte ich das übersehen ;-)

Requirements:
1. Ein Mikrocontroller soll über die serielle Schnittstelle Kommandos 
empfangen können.
2. Bei jedem Kommando "counter" soll die Instanz eines Zähler erzeugt 
werden. Verweise auf die Instanzen sollen in ein Array eingetragen 
werden.
3. Beim Empfang des Kommandos "run" soll die Exec-Function aller im 
Array befindlichen Objekte aufgerufen werden.
4. Es sollen so viele Objekte erzeugt werden können, wie in den Speicher 
passen
5. Empfängt der Mikrocontroller den Befehl 'delete' über die serielle 
Schnittstelle, sollen alle Objekte gelöscht und er Speicher wieder frei 
gegeben werden.
6. Schritt 2 soll wiederholt werden können.
7. Es soll möglich sein, das Programm zu erweitern für neue Kommados 
z.B. "generator" (Signalgenerator)

>Review machen:

von Christoph M. (mchris)


Lesenswert?

Oliver S. (oliverso)
24.02.2025 13:20
>Kein new verwendet. C++ hat Container, die übernehmen das. Wenn's
>wirklich keine vollständige C++-stdlib gibt, das ganze Konzept
>überdenken.

Ich will das Ganze auf einem Atmega328 mit 2K RAM und 32K Flash 
verwenden. Der Compiler ist AVR-GCC und ich weiß nicht, ob die stdlib 
für so kleine Controller passt.

von Christoph M. (mchris)


Angehängte Dateien:

Lesenswert?

Ich habe mal den Klassen-Code von "blink without delay" für einen Test 
verwendet.
Über die serielle Schnittstelle kann man die Kommandos

h: help
d: delete
n: new

senden.
Es werden jeweils 2 mit unterschiedlich blinkende LEDs erzeugt.
Wie man aber sieht, wächst der Speicher nach einem delete und dann new 
langsam an.
1
object: 0 address: 545  
2
object: 1 address: 563  
3
delete  
4
create new list  
5
object: 0 address: 581  
6
object: 1 address: 599

Wen ich es richtig sehe, werden wohl für jedes Object 36 Bytes 
verbraucht.
1
#define LEDARRAYT_LEN 10
2
3
Flasher *Liste[LEDARRAYT_LEN];
4
5
6
void setup()
7
{
8
  Serial.begin(115200);
9
  Idx = 0;
10
  Liste[Idx++] = new Flasher(12, 100, 400);
11
  Liste[Idx++] = new Flasher(13, 350, 350);
12
}
13
14
void loop()
15
{
16
  // run the objects is no serial char availabe
17
  while (!Serial.available())
18
    for (int n = 0; n < Idx; n++) Liste[n]->exec();
19
20
  // check the commands
21
  char c = Serial.read();
22
  
23
  if (c == 'd')
24
  {
25
    Serial.println("delete");
26
    Idx = 0;
27
    for (int n = 0; n < Idx; n++) delete(Liste[n]);
28
  }
29
  
30
  if (c == 'n')
31
  {
32
    Serial.println("create new list");
33
    Idx = 0;
34
    Liste[Idx++] = new Flasher(12, 100, 400);
35
    Liste[Idx++] = new Flasher(13, 350, 350);
36
  }
37
  
38
  if (c == 'h')
39
  {
40
    Serial.println("== help ==");
41
    Serial.println("d: delete");
42
    Serial.println("n: new list");
43
  }
44
45
}

von Michael B. (laberkopp)


Lesenswert?

Christoph M. schrieb:
> Idx = 0;
> for (int n = 0; n < Idx; n++) delete(Liste[n]);

Hâh ?

von J. S. (jojos)


Lesenswert?

Da wird ja auch nie delete aufgerufen.

von Christoph M. (mchris)


Lesenswert?

Michael B. (laberkopp)
24.02.2025 16:55

>Christoph M. schrieb:
>> Idx = 0;
>> for (int n = 0; n < Idx; n++) delete(Liste[n]);

>Hâh ?

Die Idee hier ist:
In der Liste befinden sich die Pointer auf die Objekte.
Mittels List[0] ist z.B. der Pointer auf das Object 0. Mit delete[0] 
wird der Destructor des Objects aufgerufen in der Hoffnung, dass dann 
der durch das Objekt belegte Speicher freigegeben wird.

Aber leider der Speicher wohl nicht frei gegeben.

von J. S. (jojos)


Lesenswert?

Besser als Hoffnung und Glauben sind Debugger.
Wobei es hier doch ins Auge springt: wann wird 0<0?

von Yalu X. (yalu) (Moderator)


Lesenswert?

Überleg doch mal, wie oft die Schleife durchlaufen wird :)

Das Idx=0 sollte wohl nach der Schleife stehen.

von Christoph M. (mchris)


Lesenswert?

Yalu X. (yalu) (Moderator)
>Das Idx=0 sollte wohl nach der Schleife stehen

Auf welche Zeile beziehst du dich?

von Niklas G. (erlkoenig) Benutzerseite


Angehängte Dateien:

Lesenswert?

Christoph M. schrieb:
> 2. Bei jedem Kommando "counter" soll die Instanz eines Zähler erzeugt
> werden.

Und es muss wirklich eine neue Instanz auf dem Heap angelegt werden? 
Wo merkt der Nutzer einen Unterschied, wenn in Wirklichkeit eine Instanz 
auf .data recycelt wird? Das ist sowieso der selbe SRAM, nur die 
Adressen werden anders bestimmt. Von außen ist kein Unterschied 
sichtbar, warum beziehen sich deine Anforderungen auf 
Implementationsdetails?

Christoph M. schrieb:
> Ich will das Ganze auf einem Atmega328 mit 2K RAM und 32K Flash
> verwenden.

Das ist sehr knapp für dynamische Speicherverwaltung mit new/malloc! 
Wenn du das wirklich unbedingt brauchst, solltest du einen größeren 
Controller nehmen. Am Besten gleich einen Cortex-M, dort sind 
Adressberechnungen und damit auch dynamische Speicherverwaltungen 
effizienter.

Christoph M. schrieb:
> Wen ich es richtig sehe, werden wohl für jedes Object 36 Bytes

Das kann man übrigens auch super mit sizeof() abfragen.

Ich vermute dir fehlt es ein bisschen an Erfahrungsschatz und Wissen 
über C++ um wirklich einzuschätzen, was für eine Art der 
Speicherverwaltung du brauchst. Vielleicht erläuterst du mal genau 
warum das so ist:

Christoph M. schrieb:
> Fixe Instanzen geht leider nicht.

Ich glaube so etwas wie std::inplace_vector wäre richtig für dich - es 
wird ein fixer Speicherbereich reserviert und es wird gemerkt, welche 
davon in Verwendung sind. Das ist aber erst ab C++26 verfügbar. Das geht 
nur dann nicht, wenn du den Speicher für eine ganz andere Datenstruktur 
brauchst, während keine Counter existieren. Wenn du lediglich 
verschiedene Counter-Typen unterstützten möchtest, kannst du auch 
std::variant bemühen.

Im Anhang ist eine Quick-n-Dirty Implementation von std::inplace_vector. 
Schau doch mal ob das für dich geeignet wäre. Das dort auftauchende 
"new" ist ein placement-new, d.h. es wird einfach nur der Konstruktor 
auf einem vorgegebenen Speicher aufgerufen, aber kein dynamischer 
Speicher benutzt. Du kannst neue Elemente zum Ende hinzufügen bis der 
Speicher voll ist, das letzte Element oder alle Elemente löschen, und 
die Größe abfragen. Es wird immer korrekt der Konstruktor/Destruktor der 
Objekte aufgerufen.

Grundsätzlich sollte man solche Datenstrukturen nicht auf dem Stack 
anlegen wie bei dir, daher habe ich es bei mir als globale Variable 
deklariert.

: Bearbeitet durch User
von Michael B. (laberkopp)


Lesenswert?

Niklas G. schrieb:
>> Wen ich es richtig sehe, werden wohl für jedes Object 36 Bytes
>
> Das kann man übrigens auch super mit sizeof() abfragen.

Naturlich nicht.

sizeof nennt die deklarierte Grösse der Datenstruktur in bytes, aber 
weiss weder auf welche Granularität aufgerundet wird noch welche 
Verwaltungsinformation zusätzlich auf dem heap gespeichert wird, hat 
also keine Ahnung um wie viel der heap bei jedem alloc anwachsen wird.

> Ich vermute dir fehlt es ein bisschen an Erfahrungsschatz

Bei dir. Offensichtlich.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Michael B. schrieb:
> aber
> weiss weder auf welche Granularität aufgerundet wird noch welche
> Verwaltungsinformation zusätzlich auf dem heap gespeichert wird

Richtig, ich bitte um Verzeihung, es war ungenau formuliert. Ich wollte 
natürlich sagen, dass sizeof() die Größe des Objekts inklusive Padding 
(für das Alignment von Arrays), liefert, weil ich vermutet hatte, dass 
Christoph dies wissen möchte, aber eben exklusive des 
Verwaltungsoverheads.

Michael B. schrieb:
> Bei dir. Offensichtlich.

Ja, trotz vieler Jahre Interneterfahrung vergesse ich immer wieder, 
Dinge extrem penibel zu formulieren, damit mir nicht wieder Unwissen 
vorgeworfen wird. Werde ich wohl nie lernen.

: Bearbeitet durch User
von Richard W. (richardw)


Lesenswert?

Michael B. schrieb:
> sizeof nennt die deklarierte Grösse der Datenstruktur in bytes, aber
> weiss weder auf welche Granularität aufgerundet wird

Beim AVR wird wie bei den meisten 8-Bit Architekturen nicht aufgerundet.

> noch welche Verwaltungsinformation zusätzlich auf dem heap gespeichert wird
> also keine Ahnung um wie viel der heap bei jedem alloc anwachsen wird.

Das ist für eine Abschätzung des zu erwartenden Speicherverbrauchs im 
Kontext der hier diskutierten Aufgabenstellung vernachlässigbar. Sizeof 
ist für eine grobe Abschätzung völlig ausreichend.

von Michael B. (laberkopp)


Lesenswert?

Richard W. schrieb:
> Beim AVR wird wie bei den meisten 8-Bit Architekturen nicht aufgerundet.

Bei jedem Heap wird mindestens auf die Grösse der freelist-Pointer 
aufgerundet, also 2 oder 4 byte, dazu kommt die Grössenangabe des 
Blocks, also weitere 2 byte.

Ein malloc(1) benötigt also zumindest 4 bytes vom Heap.

Du scheinst mit Compilerentwicklung und heap Struktur noch nie was zu 
tun gehabt zu haben.

: Bearbeitet durch User
von Christoph M. (mchris)


Lesenswert?

>Niklas G. (erlkoenig)

Vielen Dank für deine ausführliche Antwort und deinen Code-Beitrag :-)

>Und es muss wirklich eine neue Instanz auf dem Heap angelegt werden?
Ich vermute ja, weil die Kommandos, was angelegt werden soll, ja 
dynamisch über die serielle Schnittstelle (oder falls es auf dem PC 
läuft, von einem Konfig-File) kommen.

Siehe die Anforderungen hier:
Beitrag "Re: cpp memory leaks vermeiden"

Das ganze lasse ich auf folgenden Systemen laufen:

* Arduino Nano (Atmega328), 2k Ram, 32kFlash
gcc version 5.4.0 (GCC)
gcc version 7.3.0 (GCC)


* Pipico2, 520k Ram
gcc version 14.2.0 (GCC)

* PC, 16GB Ram
gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04)

Der Atmega hat den Vorteil, dass man ultraschnell kompilieren und testen 
kann. Außerdem zwingen die beschränkten Resourcen zur Optimierung von 
Speicherverbrauch und Rechenzeit.
Die Compilierung und der Donwload auf dem PiPico dauert ewig, deshalb 
nutze ich den immer erst, wenn ein Modul fertig ist.  Für manche 
Anwendungen brauche ich aber die Geschwindigkeit und vielleicht auch den 
Speicher.
Den PC nutze ich zur Entwicklung, insbesondere zum debuggen. Die 
Arduino-Funktionen wie "digitalWrite" sind "gestupped".

>Ich vermute dir fehlt es ein bisschen an Erfahrungsschatz und Wissen
>über C++ um wirklich einzuschätzen, was für eine Art der
>Speicherverwaltung du brauchst.

C++ verwende ich eher in rudimentärer Form.

Die Funktionalität, die ich von C++ brauche ist:
Instanzen von Klassen anlegen, die von einer übergeordneten Klasse 
abgeleitet sind, die im wesentlichen eine "init" Funktion und eine 
"exec" Funktion hat. Die Instanzen werden in einem Array eingetragen und 
dann in einem Rutsch alle "exec" Funktionen der im Array befindlichen 
Klassen aufgerufen. Das gesamte Array und die Klassen müssen in einem 
Rutsch gelöscht werden können, da bei entsprechenden Kommandos über die 
serielle Schnittstelle alles neu angelegt werden muss.
Deshalb braucht das System auch keinen "garbage collector" weil nicht 
einzelne Instanzen sondern immer das gesamte Array gelöscht werden und 
die Instanzen gelöscht werden müssen, die vermutlich schön 
hintereinander im Speicher platziert sind.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> Siehe die Anforderungen hier:

Naja da steht nur dass es dynamische Speicherverwaltung sein "soll", 
aber kein Grund dafür.

Christoph M. schrieb:
> Der Atmega hat den Vorteil, dass man ultraschnell kompilieren und testen
> kann.

Bei den STM32 und mit einem schnellen Debugger/Flasher wie J-Link ist es 
auch extrem schnell. Und debuggen kann man damit auch fantastisch.

Christoph M. schrieb:
> Die Funktionalität, die ich von C++ brauche ist:
> Instanzen von Klassen anlegen, die von einer übergeordneten Klasse
> abgeleitet sind, die im wesentlichen eine "init" Funktion und eine
> "exec" Funktion hat.

Also einfach nur Polymorphie. Dafür brauchst du immer noch keine 
dynamische Speicherverwaltung. Du kannst einfach meine o.g. 
InplaceVector Klasse mit std::variant kombinieren, dann kannst du in 
jedes Element eine der abgeleiteten Klassen packen, also z.B. als 
InplaceVector<std::variant<Derived1, Derived2, Derived3>>. Du kannst für 
jedes Element beliebig entscheiden welche Klasse drin angelegt ist.

Du brauchst dann noch nichtmal die gemeinsame Basisklasse. Über 
std::visit kannst du "init" (reicht nicht der Konstruktor?) und "exec" 
aufrufen. Du kannst die gemeinsame Basis trotzdem implementieren und 
dann noch pro Element einen Pointer darauf behalten um die Funktionen 
ohne std::visit aufzurufen, das ist dann aber etwas doppelt gemoppelt...

Christoph M. schrieb:
> Das gesamte Array und die Klassen müssen in einem Rutsch gelöscht werden
> können,

Meine InplaceVector Klasse hat dafür eine "clear" Funktion.

Christoph M. schrieb:
> Deshalb braucht das System auch keinen "garbage collector"

Den braucht man sowieso nie, auch bei wesentlich komplexeren 
Strukturen, sofern mann seine Daten immer korrekt freigibt, via 
delete/free/std::unique_ptr/std::shared_ptr. C++ hat auch iA gar keinen 
GC. Ein GC macht es nur einfacher, hat aber auch Overhead. Ist auf einem 
kleinen Mikrocontroller eh unpraktikabel.

von N. M. (mani)


Lesenswert?

Letzten Endes läuft es doch darauf raus dass du auch bei der dynamischen 
Geschichte eine maximale Menge festlegen musst. Denn einer zu viel und 
deinem uC fliegt der Deckel weg.
Außerdem musst du genau für diesen Fall den Speicher vorhalten. Overhead 
durch new/delete wurde ja auch schon genannt.

Ich würde vermutlich eine feste Menge instanziieren. Die werden wenn sie 
gebraucht werden aktiviert/parametriert. Die arbeitest du dann ab.
Beim Löschen werden sie nur initialisiert/deaktiviert.

von Roger S. (edge)


Angehängte Dateien:

Lesenswert?

Christoph M. schrieb:
> Danke. Mein Beispiel war etwas irreführend. Hier die Main noch mal neu.
> Die Liste für die Counter_IF ist 10 Einträge lang.
> Zuerst werden 5 Counter erzeugt und dann laufen lassen.
> In der zweiten Runde werden 8 Counter erzeugt. Die 5 vorher benutzen
> werden eigentlich nicht mehr benötigt,aber ich schätze, sie hängen immer
> noch im Speicher herum. Wie kann man das feststellen?

Nimm einen std::vector

Der alloziert nur speicher wenn er waechst. Du kannst neue counter mit 
der clear()/resize() combo erzeugen. Der vector arbeitet mit placement 
new, d.h. wenn der vorhandene Speicher ausreicht, wird keiner alloziert.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Roger S. schrieb:
> Der alloziert nur speicher wenn er waechst

Braucht aber trotzdem malloc/new und zieht damit eine Menge Code aus der 
Standard Bibliothek rein. Was macht man mit dem freigewordenen Speicher 
wenn man den std::vector leert? Nichts? Dann kann man auch einen 
in-place-vector nehmen. Ganz nebenbei müssen die Elemente dafür auch 
nicht kopierbar/move-bar sein.

von Roger S. (edge)


Lesenswert?

Niklas G. schrieb:
> Roger S. schrieb:
>> Der alloziert nur speicher wenn er waechst
>
> Braucht aber trotzdem malloc/new und zieht damit eine Menge Code aus der
> Standard Bibliothek rein.

und? was ist das Problem hier?

> Was macht man mit dem freigewordenen Speicher
> wenn man den std::vector leert? Nichts?

wenn man den vector auf der maximalen Groesse belaesst, dann hat man 
keine allokationen mehr, kann in einem embedded system von Vorteil sein.

> Dann kann man auch einen
> in-place-vector nehmen.

kann man, wenn man den dann hat. Out-of-the-box support sieht schlecht 
aus.

> Ganz nebenbei müssen die Elemente dafür auch
> nicht kopierbar/move-bar sein.

nope.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Roger S. schrieb:
> und? was ist das Problem hier?

Ist einfach unnötige Verschwendung von Flash, Rechenzeit und RAM.

Roger S. schrieb:
> wenn man den vector auf der maximalen Groesse belaesst, dann hat man
> keine allokationen mehr, kann in einem embedded system von Vorteil sein

Was ist der Vorteil gegenüber der Lösung, den Speicher einfach direkt 
vom Linker allokieren zu lassen?

Roger S. schrieb:
> kann man, wenn man den dann hat. Out-of-the-box support sieht schlecht
> aus.

Aber wie gezeigt kann man sich den sehr leicht selbst implementieren und 
hat dann minimalen Aufwand in RAM, Flash, Rechenzeit.

Roger S. schrieb:
> nope.

Hmm: https://godbolt.org/z/zecW614Kd

Okay, wenn man nie Elemente hinzufügt, braucht man keine move/copy 
Konstruktoren/Zuweisungsoperatoren. Aber ich glaub das hilft hier nicht.

von Roger S. (edge)


Lesenswert?

Niklas G. schrieb:
> Roger S. schrieb:
>> und? was ist das Problem hier?
>
> Ist einfach unnötige Verschwendung von Flash, Rechenzeit und RAM.

gibt es Geld zurueck fuer nicht genutzen Flash?
Was meinst du wie komplex der std::vector code hier ist, verglichen mit 
deinem inplace vector?

> Was ist der Vorteil gegenüber der Lösung, den Speicher einfach direkt
> vom Linker allokieren zu lassen?

Standard Library Komponente. Einziger Nachteil ist dass der 
Speicherbedarf nicht zu compile-zeit ermittelt wird. Ansonsten 
identisch.

> Aber wie gezeigt kann man sich den sehr leicht selbst implementieren und
> hat dann minimalen Aufwand in RAM, Flash, Rechenzeit.

Leicht? Fuer einen Anfaenger verstaendlich? Der RAM, Flash und 
Rechenzeit vergleich hinkt.

> Hmm: https://godbolt.org/z/zecW614Kd
> Okay, wenn man nie Elemente hinzufügt, braucht man keine move/copy
> Konstruktoren/Zuweisungsoperatoren. Aber ich glaub das hilft hier nicht.

Weiss nicht was du damit zum Ausdruck bringen willst, aber: 
https://godbolt.org/z/YzGxvc9PM

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Roger S. schrieb:
> gibt es Geld zurueck fuer nicht genutzen Flash?

Nein, aber man kann mehr Funktionalität rein packen.

Roger S. schrieb:
> Was meinst du wie komplex der std::vector code hier ist, verglichen mit
> deinem inplace vector?

Größenordnungen komplexer. Allein schon für malloc. Ein Quick-And-Dirty 
Vergleich für Cortex-M4 (vector gibt's ja nicht beim AVR-GCC), mit einem 
dummy-print statt iostream um dessen Overhead nicht mitzunehmen:
1
   text    data     bss     dec     hex filename
2
   9060      92    1908   11060    2b34 vector.elf
3
   1004       4    1652    2660     a64 inplace.elf

Der statisch allokierte RAM-Platz ist für den in-place vector kleiner 
als allein nur der Overhead für die Implementation vom std::vector auf 
dem statisch allokierten RAM (.data, .bss).

Roger S. schrieb:
> Standard Library Komponente.

Ist beim AVR-GCC eh nicht verfügbar.

Roger S. schrieb:
> Ansonsten
> identisch.

Bis auf Flash,Ram,Rechenzeit halt, aber wen interessieren die schon, 
gerade auf einem AVR.

Roger S. schrieb:
> Leicht? Fuer einen Anfaenger verstaendlich?

So schlimm ist es nicht. Ein Anfänger könnte es auch ohne Template 
machen, dann bleibt nicht mehr viel übrig. Den iterator kann man sich 
sparen und immer "zu Fuß" iterieren.

Roger S. schrieb:
> Weiss nicht was du damit zum Ausdruck bringen willst, aber:

Diese Klasse hat den impliziten Copy/Move 
Konstruktur/Zuweisungsoperator. Aber den hat man nicht immer, in meinem 
Beispiel hab ich diese explizit "deleted". Oft es es auch nicht 
praktikabel, Copy/Move zu implementieren. Und dann kann man einen 
vector, der diese Klasse enthält, zwar anlegen, aber man kann keine 
Elemente hinzufügen.

: Bearbeitet durch User
von Christoph M. (mchris)


Lesenswert?

Vielen Dank für eure Beiträge.

Ich habe mal als einfacher Test den Code von Niclas
https://www.mikrocontroller.net/attachment/highlight/662012
mit den verschiedenen Systemen compiliert, auf dem das laufen soll.

PC: geht fehlerfrei.

Arduino IDE, Board Arduino Nano:
1
Detecting libraries used...
2
/home/christoph/tools/Arduino/arduino-1.8.6/hardware/tools/avr/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -flto -w -x c++ -E -CC -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10806 -DARDUINO_AVR_NANO -DARDUINO_ARCH_AVR -I/home/christoph/tools/Arduino/arduino-1.8.6/hardware/arduino/avr/cores/arduino -I/home/christoph/tools/Arduino/arduino-1.8.6/hardware/arduino/avr/variants/eightanaloginputs /tmp/arduino_build_60317/sketch/TEST_objectDelete.ino.cpp -o /dev/null
3
TEST_objectDelete:1:19: error: cstddef: No such file or directory
4
compilation terminated.
5
exit status 1
6
cstddef: No such file or directory

Arduino IDE, Board PiPico:
1
Sketch wird kompiliert...
2
....
3
TEST_objectDelete:11:17: error: 'constexpr InplaceVector<T, N>::Container::Container()' cannot be overloaded with 'constexpr InplaceVector<T, N>::Container::Container()'
4
   11 |       constexpr Container () {}
5
      |                 ^~~~~~~~~
6
/home/christoph/Entwicklung/250204_GraphProg2025/ArduinoVM/TEST_objectDelete/TEST_objectDelete.ino:11:11: note: previous declaration 'constexpr InplaceVector<T, N>::Container::Container()'
7
   11 |       constexpr Container () {}
8
      |           ^~~~~~~~~
9
/home/christoph/Entwicklung/250204_GraphProg2025/ArduinoVM/TEST_objectDelete/TEST_objectDelete.ino:11:11: warning: inline function 'constexpr InplaceVector<T, N>::Container::Container() [with T = Counter_IF; unsigned int N = 8]' used but never defined
10
exit status 1
11
'constexpr InplaceVector<T,

von Niklas G. (erlkoenig) Benutzerseite


Angehängte Dateien:

Lesenswert?

Christoph M. schrieb:
> Arduino IDE, Board Arduino Nano:

Ja, der AVR-GCC liefert keine C++ Standard-Bibliothek mit. Im Anhang ist 
eine Variante die die fehlenden Sachen mitliefert. Ist natürlich ein 
recht fieser Workaround.

Christoph M. schrieb:
> constexpr InplaceVector<T, N>::Container::Container()' cannot be
> overloaded with 'constexpr InplaceVector<T, N>::Container::Container()'

Ah, die Arduino-DIE verunstaltet, äh, vorverarbeitet den Code und 
verschluckt sich offenbar am Konstruktor der "Container" Klasse und fügt 
dort die Prototypen von setup und loop ein. Im angehängten Code ist dies 
repariert indem einfach der Konstruktur nicht inline ist.

Christoph M. schrieb:
> Die Compilierung und der Donwload auf dem PiPico dauert ewig

Bei mir war es nur beim ersten Mal langsam, danach wird der beim letzten 
Mal kompilierte Bibliothekscode wiederverwendet. Dann sind es nur noch 
ein paar Sekunden, die sich die Arduino-IDE selbst genehmigt, während 
der Compiler nur einen Bruchteil dieser Zeit ausmacht. Bei einer 
vernünftigen™ IDE sieht das natürlich besser aus.

von Christoph M. (mchris)


Lesenswert?

> Niklas G. (erlkoenig) Benutzerseite
Vielen Dank für das Programm. Es kompiliert in der Arduino-IDE 
fehlerfrei.
Ich werde die nächsten Tage mal versuchen, das Ganze genauer zu 
verstehen und etwas anzupassen.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> Vielen Dank für das Programm. Es kompiliert in der Arduino-IDE
> fehlerfrei.

Super!

In der gezeigten Version müssen wie gesagt alle Elemente vom selben Typ 
sein, aber man kann per std::variant jeweils eine von mehreren Klassen 
auswählen. Optional kannst du dann:

Niklas G. schrieb:
> Du kannst die gemeinsame Basis trotzdem implementieren und
> dann noch pro Element einen Pointer darauf behalten um die Funktionen
> ohne std::visit aufzurufen, das ist dann aber etwas doppelt gemoppelt...

Eine abgewandelte Möglichkeit habe ich mal umgesetzt, und zwar eine 
Klasse sehr ähnlich zu std::variant, die immer genau eine Instanz aus 
einer Liste von N Klassen enthält, welche aber eine gemeinsame 
Basisklasse haben müssen. Meine Klasse speichert einen Pointer auf die 
Basis der jeweils erzeugten Instanz, welchen man dann abfragen kann um 
virtuelle Funktionen aufzurufen, wobei dann aber std::visit nicht mehr 
unterstützt wird. Das ist also Polymorphie bei statisch allokiertem 
Speicher. Letztendlich ist es eine union+Pointer mit schönerer Syntax 
drumherum.

Das Ganze hatte ich benutzt um das State Pattern zu implementieren, aber 
auf deinen Use Case würde es auch gut passen. Der Code ist hier:

https://github.com/Erlkoenig90/OverlayFSM/blob/main/inc/OverlayFSM/overlay_fsm.hh

Das müsste man auch wieder recht gut auf AVR-GCC anpassen können indem 
man die fehlenden Standard-Bibliotheks-Komponenten manuell hinzufügt. 
Der Code ist mittlerweile etwas alt, vermutlich kann man da bei 
aktuellen Compilern schon etwas aufpolieren/kürzen.

Damit könntest du dann z.B. sowas machen:
1
InplaceVector<OverlayFSM<Base, Derived1, Derived2, Derived3>> counters;
2
3
int main () {
4
  // Derived1-Instanz hinzufügen
5
  counters.emplace_back (static_cast<Derived1*> (nullptr), ... /* Konstruktor-Parameter für Derived1 */ );
6
7
  // Derived2-Instanz hinzufügen
8
  counters.emplace_back (static_cast<Derived2*> (nullptr), ... /* Konstruktor-Parameter für Derived2 */ );
9
  
10
  // 2. Instanz auf Derived1 ändern
11
  counters [1].go <Derived1> (... /* Konstruktor-Parameter für Derived1 */);
12
13
  // virtuelle Funktionen aufrufen
14
  for (Base& obj : counters)
15
    obj.current ().exec ();
16
  
17
  // Alle löschen
18
  counters.clear ();
19
}

von Veit D. (devil-elec)


Lesenswert?

Hallo,

wenn es um eine avr LibStdCpp geht, die hier kann man verwenden.
https://github.com/modm-io/avr-libstdcpp

Zum Testen wegen Speicherleck. Man kann doch die Adresse der Instanz 
ermitteln. Dann kann man ein paar Speicherzellen sequentiell lesen und 
mit vorher/nachher den Inhalt vergleichen. Ob das Aussagekräftig ist 
weiß ich nicht. Denn überschrieben wird der freigegebene Speicher erst 
wenn Bedarf besteht.

Zudem, wie soll jemand auf einen µC an den Speicher rankommen zum 
auslesen? Man kann doch nachträglich keine Software zur Spionage 
installieren wie das auf einem PC möglich wäre.

: Bearbeitet durch User
von Pandur S. (jetztnicht)


Lesenswert?

Ich wuerde dynamischen Speicher vermeiden. Alles statisch anlegen und 
wiedervewenden.
Was gewinnt man mit dynamischem Speicher ?

von Harald K. (kirnbichler)


Lesenswert?

Pandur S. schrieb:
> Was gewinnt man mit dynamischem Speicher ?

Flexibilität, die man aber nicht in jedem Fall braucht.

von Richard W. (richardw)


Lesenswert?

Diese Flexibilität beim programmieren sollte man nicht unterschätzen, 
besonders wenn man sich als Ingenieur mal vor Augen führt was so ein 
Arbeitstag kostet.

Man kann ja auch nur während der Initialisierung des Programms dynamisch 
allozieren und dann zur Laufzeit nichts mehr daran ändern. Dann weiß man 
zur Startzeit sicher dass der Speicher reicht. Das ist zum Beispiel eine 
Lösung für DSP-artige Software wo der gleiche Code auf vielen 
Plattformen inkl PC (Simulation) laufen soll. Oder wenn die Firmware 
verschiedene Aufgaben erfüllen soll und das beim Start irgendwie 
ausgewählt wird.

Ich habe auch schon gesehen dass manche einen Bereich statisch 
reservieren und dort dann ihre eigene dynamische Speicherverwaltung 
nachbauen. Aber da kann man auch in der Regel auch gleich den 
existierenden Heap von libc bzw. des OS nehmen und muss nicht zig 
Arbeitsstunden versenken.

Ich denke dass man die Nutzung des Heap nicht kategorisch ausschließen 
sollte, auch nicht auf kleinen Mikrocontrollern. Wenn man seine Tools 
kennt und sich auch in libc und den ganzen low-Level Routinen zurecht 
findet, dann kann man auch gute Entscheidungen treffen und muss nicht 
irgendwelche Weisheiten aus dem Internet unhinterfragt glauben.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Oder wenn die Firmware verschiedene Aufgaben erfüllen soll und das beim
> Start irgendwie ausgewählt wird.

Da kann man auch einfach ein std::variant der diversen Hauptklassen für 
die einzelnen Use Cases nehmen.

Richard W. schrieb:
> Man kann ja auch nur während der Initialisierung des Programms dynamisch
> allozieren

Und wo besteht dann der Vorteil davon, gegenüber dem simplen Anlegen als 
globale (static) Variablen?

Richard W. schrieb:
> besonders wenn man sich als Ingenieur mal vor Augen führt was so ein
> Arbeitstag kostet.

Die Arbeitszeit, die das Debuggen kostet, weil man bei irgendeinem 
malloc() das Prüfen auf NULL vergessen hat, ist auch nicht 0...

von Richard W. (richardw)


Lesenswert?

Niklas G. schrieb:
> Richard W. schrieb:
>> Oder wenn die Firmware verschiedene Aufgaben erfüllen soll und das beim
>> Start irgendwie ausgewählt wird.
>
> Da kann man auch einfach ein std::variant der diversen Hauptklassen für
> die einzelnen Use Cases nehmen.

Möglich, `std::variant` ist auch schnell mal ausprobiert und dann sieht 
man ja ob es passt.

>
> Richard W. schrieb:
>> Man kann ja auch nur während der Initialisierung des Programms dynamisch
>> allozieren
>
> Und wo besteht dann der Vorteil davon, gegenüber dem simplen Anlegen als
> globale (static) Variablen?

Dass die Größe/Anzahl der Objekte/whatever von Parametern abhängen kann 
und diese Parameter erst während der Laufzeit bekannt sind.

>
> Richard W. schrieb:
>> besonders wenn man sich als Ingenieur mal vor Augen führt was so ein
>> Arbeitstag kostet.
>
> Die Arbeitszeit, die das Debuggen kostet, weil man bei irgendeinem
> malloc() das Prüfen auf NULL vergessen hat, ist auch nicht 0...

Das streite ich nicht ab, gebe aber zu bedenken, dass Leute die 
`std::variant` als Lösung auch nur in Betracht ziehen, auch dazu neigen, 
kein `malloc()` händisch zu verwenden. In Bare-Metal Software würde man 
eher `std::__throw_bad_alloc()` überschreiben und bekäme so implizit 
Bescheid wenn der Heap alle ist. Mit dem ld Linker und dem nützlichen 
flag `--wrap` könnte man sich auch einfach in `malloc()` einklinken. 
Gibt viele Möglichkeiten.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Dass die Größe/Anzahl der Objekte/whatever von Parametern abhängen kann
> und diese Parameter erst während der Laufzeit bekannt sind.

Was machst du dann mit dem Speicher der nicht benutzt wird weil die 
Parameter klein waren?

Richard W. schrieb:
> In Bare-Metal Software würde man
> eher `std::__throw_bad_alloc()` überschreiben

Hast du da mal ein Beispiel, z.B. für AVR und Cortex-M 
(GCC-ARM-Embedded)?

von Richard W. (richardw)


Lesenswert?

Niklas G. schrieb:
> Richard W. schrieb:
>> Dass die Größe/Anzahl der Objekte/whatever von Parametern abhängen kann
>> und diese Parameter erst während der Laufzeit bekannt sind.
>
> Was machst du dann mit dem Speicher der nicht benutzt wird weil die
> Parameter klein waren?

Möchten Sie darauf hinaus, dass man dann auch gleich statisch ein 
Kontextobjekt für alle Parametervarianten allozieren könnte, verpackt in 
std::variant wie Sie bereits weiter oben vorgeschlagen haben? Auf jeden 
Fall kann man das machen. Mir geht es darum, dass man auch Klassen wie 
std::vector ohne Aufwand im Code verwenden kann. Ob das nun im 
Speziellen auf dem AVR so sinnvoll ist, sei mal dahin gestellt. Es macht 
aber den Code deutlich einfacher. Ihre Lösung mit dem globalen 
Kontextobjekt (ich interpretiere das jetzt einfach mal so) ist auf jeden 
Fall eine Möglichkeit wenn der Code ausschließlich als Firmware in einem 
eingebetteten System laufen soll. Spart dann oft auch noch Codegröße. 
Aber nehmen wir mal an, es handelt sich um eine DSP Applikation und 
jetzt wollen Sie zwei Instanzen desselben Codes zu Testzwecken in einer 
PC-Applikation laufen lassen - z.B. Sender und Empfänger, dann kann man 
nicht einfach globale Variablen nehmen. Das ist selbstverständlich alles 
lösbar, erzeugt aber Entwicklungskosten. Und da kann es manchmal bei 
aller Bastelfreudigkeit einfacher sein, den Heap zu verwenden wie er 
ist. Letztendlich ist der Heap ja auch ein statischer Block Speicher mit 
bekannter Größe und vorimplementierter Speicherverwaltung. Man sieht die 
Auslastung aber erst zur Laufzeit - bestenfalls nach der 
Initialisierung.

> Richard W. schrieb:
>> In Bare-Metal Software würde man
>> eher `std::__throw_bad_alloc()` überschreiben
>
> Hast du da mal ein Beispiel, z.B. für AVR und Cortex-M
> (GCC-ARM-Embedded)?

Die Funktion std::__throw_bad_alloc() wird vom Operator new in 
libstdc++v3 (GCC) aufgerufen und ist Teil von ebendieser. Man kann sie 
überschreiben und zum Programm dazu linken. Aber wie gesagt, ein --wrap 
von malloc() ist noch besser weil man dann auch C code mit abdeckt.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Möchten Sie darauf hinaus, dass man dann auch gleich statisch ein
> Kontextobjekt für alle Parametervarianten allozieren könnte, verpackt in
> std::variant wie Sie bereits weiter oben vorgeschlagen haben?

Ja. Hat bisher immer perfekt funktioniert.

Richard W. schrieb:
> Aber nehmen wir mal an, es handelt sich um eine DSP Applikation und
> jetzt wollen Sie zwei Instanzen desselben Codes zu Testzwecken in einer
> PC-Applikation laufen lassen - z.B. Sender und Empfänger, dann kann man
> nicht einfach globale Variablen nehmen.

Auch dann funktioniert das wunderbar, man legt einfach mehr Objekte an. 
Statt 2x malloc hat man eben 2 Zeilen mit den globalen Objekten. Auf dem 
PC kann man natürlich für diesen Zweck auch "new" nutzen, da spielt es 
keine Rolle.

Die Objekte sind zwar im globalen Speicher (letztendlich .data / .rodata 
/ .bss), aber man sollte sie nicht im klassischen C-Sinne wie globale 
Objekte benutzen, sondern nur möglichst wenigen Stellen direkt darauf 
zugreifen. Typischerweise 1-2x in der main(), und ggf. noch in 
Interrupts. Das lässt sich z.B. erreichen inden man die Objekte als 
"static" anlegt in der main.cpp, welche ausschließlich die main() und 
die ISRs enthält. Alle anderen Zugriffe erfolgen indirekt über 
Referenzen/Pointer. Somit kann man ganz problemlos Objekte 
hinzufügen/ändern, man muss nur die ganz wenigen Zugriffe erweitern. Das 
muss man bei der dynamischen Allokation per "new" auch.

Richard W. schrieb:
> Man sieht die
> Auslastung aber erst zur Laufzeit

Das ist für mich schon ein Knackpunkt, hat schon Vorteile wenn der 
Linker dies direkt ausgeben kann.

Richard W. schrieb:
> Man kann sie
> überschreiben und zum Programm dazu linken.

Also muss man die Standardbibliothek neu kompilieren?

Richard W. schrieb:
> Aber wie gesagt, ein --wrap
> von malloc() ist noch besser weil man dann auch C code mit abdeckt.

Naja, wenn man im Fehlerfall von malloc() z.B. das Programm anhält, 
passiert das auch in Fällen, wo der Rückgabewert von malloc() korrekt 
geprüft wird. Man möchte ja die Stellen finden, wo das nicht passiert.

von Richard W. (richardw)


Lesenswert?

Niklas G. schrieb:
> Richard W. schrieb:
>> Man kann sie
>> überschreiben und zum Programm dazu linken.
>
> Also muss man die Standardbibliothek neu kompilieren?

Nein. Symbole die beim linken in den Object Dateien definiert sind, 
haben immer Vorrang gegenüber Symbolen aus statischen Bibliotheken.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Nein. Symbole die beim linken in den Object Dateien definiert sind,
> haben immer Vorrang gegenüber Symbolen aus statischen Bibliotheken.

Okay, kannst du dann mal ein Beispiel zeigen wie man das im User Code 
überschreibt?

von Oliver S. (oliverso)


Lesenswert?

Niklas G. schrieb:
> Richard W. schrieb:
>> Nein. Symbole die beim linken in den Object Dateien definiert sind,
>> haben immer Vorrang gegenüber Symbolen aus statischen Bibliotheken.
>
So pauschal ist das falsch...


> Okay, kannst du dann mal ein Beispiel zeigen wie man das im User Code
> überschreibt?

Als Einstieg:
https://www.gnu.org/software/libc/manual/html_node/Replacing-malloc.html

Oliver

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Oliver S. schrieb:
> Als Einstieg:
> https://www.gnu.org/software/libc/manual/html_node/Replacing-malloc.html

Am Meisten würde mich das für std::__throw_bad_alloc() interessieren...

von Richard W. (richardw)


Lesenswert?

Niklas G. schrieb:
> Am Meisten würde mich das für std::__throw_bad_alloc() interessieren...

Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest, 
sorge dafür dass folgende Funktion in einer Objektdatei mit zur 
Applikation gelinkt wird:
1
// cxxabi_custom.cpp
2
namespace std {
3
    void __throw_bad_alloc() {
4
        // Mach irgendwas sinnvolles
5
        asm volatile ("nop");
6
    }
7
}

Stecke sie nicht in eine statische Bibliothek. Und dann schaust du mal 
mit dem Debugger ob du dort im Fehlerfall landest und wenn nicht, warum 
nicht. Die Funktion ist ursprünglich hier definiert:

https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/src/c%2B%2B11/functexcept.cc

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
> sorge dafür dass folgende Funktion in einer Objektdatei mit zur
> Applikation gelinkt wird:

Mit -Wl,--gc-sections landet die Funktion bei mir so nicht bei mir im 
Assembly-Listing (wird wegoptimiert), funktioniert also nicht. Ohne 
-Wl,--gc-sections wird sie zwar natürlich nicht wegoptimiert, wird aber 
nie aufgerufen oder als Funktionszeiger genutzt. Mit dem 
GCC-ARM-Embedded für Cortex-M.

: Bearbeitet durch User
von Oliver S. (oliverso)


Lesenswert?

Richard W. schrieb:
> Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,

Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um 
einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da 
aber keine.

Oliver

: Bearbeitet durch User
von Christoph M. (mchris)


Lesenswert?

Oliver S. (oliverso)
11.03.2025 16:21
>Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um
>einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da
>aber keine.

Nicht ganz. Es geht um ein "Design Pattern" mit dem man eine Liste von 
Objectinstanzen anlegen kann und dann die gesamte Liste wieder löschen 
kann.

Die Zielarchitekturen sind dabei:
- Atmega328
- PiPico
- PC

Das Design Pattern soll auf allen Architekturen funktionieren.

von Richard W. (richardw)


Lesenswert?

Oliver S. schrieb:
> Richard W. schrieb:
>> Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
>
> Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um
> einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da
> aber keine.
>
> Oliver

Halte ich für teilweise richtig und wir kompilieren hier im Thread auch 
mit -fno-exceptions. Aber die Exception handler werden trotzdem 
aufgerufen, werfen dann nur keine Exceptions sondern rufen 
std::terminate() und letztendlich abort() auf. Sieht man zum Beispiel 
hier: 
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/libsupc%2B%2B/new_op.cc;h=a45408b349ab3eabc905e1575586ab59af7de3f8;hb=HEAD

Das Macro wird hier definiert, abhängig davon ob Exceptions aktiviert 
sind: 
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/include/bits/c%2B%2Bconfig;h=676f5eecbbb6a715800a99aa4976fd8e51826dde;hb=HEAD#l260

Obiger Link erklärt auch, warum Niklas keinen Aufruf von 
std::__throw_bad_alloc() sieht. Nicht die Implementierung von operator 
new() ruft std::__throw_bad_alloc() auf sondern der Standardallokator 
std::new_allocator der auch von den std Container genutzt wird. Sobald 
man zum Beispiel std::vector benutzt, ist die Funktion dabei, bei einem 
einfachen new MeineKlasse{} aber nicht. Mea culpa.

Bleiben die Möglichkeiten, (1) die diversen Operator new mit einer 
eigenen Implementierung zu ersetzen oder (2) sich in __cxa_throw() 
einzuklinken bzw. zu ersetzen und auf einen bad_alloc() Parameter zu 
prüfen oder (3) sich mit --wrap gleich in malloc() einzuklinken welches 
am Ende immer aufgerufen wird und dort zu prüfen ob die Allokation 
erfolgreich war.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Bleiben die Möglichkeiten

Klingt alles ziemlich umständlich. In der Zeit in der wir das hier alles 
durchwühlen hätte man das lockerst als globale Variable angelegt, ...

Richard W. schrieb:
> besonders wenn man sich als Ingenieur mal vor Augen führt was so ein
> Arbeitstag kostet.

von Richard W. (richardw)


Lesenswert?

Hehe, volle Zustimmung. Deine Lösung mit std::variant oder einem
 dummen union ist in Zuverlässigkeit und Vorhersagbarkeit natürlich 
allem anderen überlegen. Aber es wird der Tag kommen wo du ein Stück 
Software vorgesetzt bekommst und irgend einen dummen Fehler bei der 
dynamischen Speicherallokation finden sollst.

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Richard W. schrieb:
> Aber es wird der Tag kommen wo du ein Stück Software vorgesetzt bekommst
> und irgend einen dummen Fehler bei der dynamischen Speicherallokation
> finden sollst.

Bei solchen Fällen würde man wahrscheinlich eh nicht den Code 
modifizieren sondern Breakpoints in malloc() setzen. Solche Fehler sind 
ja gern mal "Heisenbugs"...

von Oliver S. (oliverso)


Lesenswert?

Richard W. schrieb:
> Oliver S. schrieb:
>> Richard W. schrieb:
>>> Wenn du die libstdc++v3 (also klassische GCC-Toolchain) verwendest,
>>
>> Um das nur kurz im Kontext des Threads hier zu bewerten: hier geht's um
>> einen AVR mit Arduino. Das ist zwar gcc und C++, Exceptions gibt's da
>> aber keine.
>>
>> Oliver
>
> Halte ich für teilweise richtig und wir kompilieren hier im Thread auch
> mit -fno-exceptions.

Vielleicht noch etwas deutlicher: AVR Arduino ist zwar C++, aber ohne 
jegliche libstdc++. Da gibts nur die Arduino-lib, die etwas C++-Flair 
verbreitet, ohne exceptions, handler, und sonstiges.

Oliver

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Oliver S. schrieb:
> handler

Meinst du Exception-Handler (catch-Blöcke, Stack Unwinding)?

von Oliver S. (oliverso)


Lesenswert?

Ich meine die

Richard W. schrieb:
> Aber die Exception handler werden trotzdem
> aufgerufen, werfen dann nur keine Exceptions sondern rufen
> std::terminate() und letztendlich abort() auf.

Es gibt für AVRs überhaupt gar keine keine libstdc++, und keine 
Exception handler, exceptions, oder irgend etwas in der Art. Damit ist 
die ganze Diskussion hier müßig.

Oliver

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Oliver S. schrieb:
> Es gibt für AVRs überhaupt gar keine keine libstdc++, und keine
> Exception handler, exceptions, oder irgend etwas in der Art

Exception Handler übersetzt der AVR-GCC sehr wohl (wenn man -fexceptions 
angibt):
1
void test () {
2
    try {
3
        foo ();
4
    } catch (int err) {
5
        puts ("Error");
6
    }
7
}

Man kann halt nur nichts damit machen weil es kein Unwinding gibt. 
Theoretisch könnte man das implementieren...

von Richard W. (richardw)


Lesenswert?

Oliver S. schrieb:
> Es gibt für AVRs überhaupt **gar keine** keine libstdc++,

https://github.com/modm-io/avr-libstdcpp

Du meintest vielleicht, dass die GCC Toolchain das nicht mitbringt.

> und keine Exception handler

https://github.com/modm-io/avr-libstdcpp/blob/master/src/functexcept.cc

Es tut mir sehr leid, dass ich diese Funktionen Exception Handler 
genannt habe. Das war wirklich sehr missverständlich von mir 
ausgedrückt. In der GNU libstdc++ sollen diese Funktionen die 
eigentlichen Exceptions werfen, sofern Exceptions aktiviert sind. 
Ansonsten terminieren sie das Programm wie im Link gezeigt.

von Oliver S. (oliverso)


Lesenswert?

Richard W. schrieb:
> https://github.com/modm-io/avr-libstdcpp

Ist eine partielle Implementierung von ein paar wenigen Features. Gut 
gemacht, und sinnvoll, aber keine libstdc++, und natürlich ohne jegliche 
Exceptions.

Niklas G. schrieb:
> Man kann halt nur nichts damit machen weil es kein Unwinding gibt.
> Theoretisch könnte man das implementieren...

Vermutlich könnte man das sogar praktisch, hat nur noch niemand gemacht. 
Obs auf einem AVR sinnvoll ist, ist allerdings fraglich.

Oliver

von Oliver S. (oliverso)


Lesenswert?

Niklas G. schrieb:
> xception Handler übersetzt der AVR-GCC sehr wohl (wenn man -fexceptions
> angibt):
> void test () {
>     try {
>         foo ();
>     } catch (int err) {
>         puts ("Error");
>     }
> }

Ja, und weils nutzlos ist, optimiert der das dann auch gleich alles 
spurlos weg.
Wollte mans wirklich nutzen, und packt in foo() ein throw dazu, stolpert 
der linker über die fehlenden lib-Funktionen.

Es ist, wie es ist: C++ auf dem AVR ist ein stark eingeschränkter 
Sonderfall, ohne  stdlibc++ und Exceptions.

Oliver

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Oliver S. schrieb:
> Ja, und weils nutzlos ist, optimiert der das dann auch gleich alles
> spurlos weg

Bei mir nicht, wenn foo() nicht definiert, nur deklariert ist.

Oliver S. schrieb:
> Wollte mans wirklich nutzen, und packt in foo() ein throw dazu, stolpert
> der linker über die fehlenden lib-Funktionen.

So ist es... aber der Compiler kann mit den Exception Handlern 
grundsätzlich umgehen.

Oliver S. schrieb:
> Es ist, wie es ist: C++ auf dem AVR ist ein stark eingeschränkter
> Sonderfall, ohne  stdlibc++ und Exceptions.

Ja, selbst auf Cortex-M ist es zwar möglich aber wenig sinnvoll. 
Zumindest bei der GCC-ARM-Embedded Toolchain hat es einen großen 
Overhead bei sehr wenig Nutzen; die Art von Fehler und Fehlerbehandlung, 
für die Exceptions konzipiert sind, tritt bei Embedded Systemen einfach 
kaum auf. Daher schrieb ich ja schon: Speicher statisch allozieren.

Höchstens vielleicht wenn man sowas wie eine SPS implementiert, wo man 
aufgrund einer Konfigurationsdatei dynamisch ein Modell aufbaut; dann 
nutzt man den Speicher entweder für 3 PID Regler oder ein Moving Average 
oder oder... Für die kurze Phase des Modell-Aufbaus können Exceptions 
den Code vermutlich vereinfachen, aber ob sich das lohnt?

von Christoph M. (mchris)


Lesenswert?

Interessant wäre, wie viel Overhead C++ erzeugt. Es gibt ja Leute die 
behaupten, man könne C++ so programmieren, dass kein Overhead entsteht. 
Bei meine Experimenten schien es aber so zu sein, dass selbst nur das 
Einbinden der Objektblibliotheken ohne eine einzige erzeugte Instanz 
schon RAM verbraucht hat. Die Objektinstanzen selbst fressen dann 
mindestens 16 Bytes selbst wenn sie keine lokale Variablen haben.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> Interessant wäre, wie viel Overhead C++ erzeugt.

Beim GCC-ARM-Embedded + STM32 Setup: 52 Bytes Flash, für die Schleife im 
Reset_Handler um die Konstruktoren globaler Objekte aufzurufen. Die wird 
nicht wegoptimiert wenn keine globalen Objekte vorhanden sind (wäre 
interessant zu analysieren ob das mit dem GNU LD ginge).

Christoph M. schrieb:
> Es gibt ja Leute die
> behaupten, man könne C++ so programmieren, dass kein Overhead entsteht.

Da gehöre ich zu, bis auf diese 52 Bytes. Es kommt natürlich stark 
darauf an, welche Sprachfeatures man verwendet. Exceptions haben wie 
gesagt auf Mikrocontrollern einen großen Overhead gegenüber 
traditioneller Fehlerbehandlung. Bei PC/Server-Programmierung sieht das 
wieder ganz anders aus, da kann Exception-basierter Code sogar 
schneller sein. Der Unwinding-Code existiert dann genau 1x auf dem 
System, in der libstdc++.

Christoph M. schrieb:
> Bei meine Experimenten schien es aber so zu sein, dass selbst nur das
> Einbinden der Objektblibliotheken ohne eine einzige erzeugte Instanz
> schon RAM verbraucht hat.

Ich denke mal du meinst mit Objektbibliothek eine gewöhnliche statische 
Bibliothek (.a / .o) in welcher C++ Klassen implementiert sind (d.h. der 
Code der Memberfunktionen).

Dann solltest du mal mit -ffunction-sections -fdata-sections kompilieren 
und mit -Wl,--gc-sections linken. Dann werden nicht genutzte globale 
Objekte wegoptimiert, außer der Autor hat "__attribute__((used))" dran 
geschrieben.

Das ist natürlich 100% unabhängig von C vs. C++, auch statische 
C-Libraries können globale Variablen beinhalten die dann beim Einbinden 
natürlich direkt RAM belegen. Eigentlich sind gerade C-Bibliotheken 
gern so implementiert.

Christoph M. schrieb:
> Die Objektinstanzen selbst fressen dann
> mindestens 16 Bytes selbst wenn sie keine lokale Variablen haben.

Lokale Variablen gibt es nur innerhalb von Funktionen, nicht in 
Objekten. C++ Objekte sind, genau wie C-Variablen, mindestens 1 Byte 
groß. Das muss so, damit Zeiger auf verschiedene Instanzen 
unterschiedlich sind, genau wie in C. Außer man verwendet alignas(), um 
ein Alignment von z.B. 16 Byte zu erzwingen, genau wie in C. Mit 
"no_unique_address" kann man in C++ dafür sorgen, dass mehrere leere 
Member-Variablen innerhalb einer Klasse die Größe 0 bekommen.

Dank templates und inlining kann C++ Code durchaus schneller als 
äquivalenter C-Code sein, bzw. ein gleich schneller C-Code wäre extrem 
umständlich zu implementieren.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Oliver S. schrieb:
> Ich meine die
>
> Richard W. schrieb:
>> Aber die Exception handler werden trotzdem
>> aufgerufen, werfen dann nur keine Exceptions sondern rufen
>> std::terminate() und letztendlich abort() auf.
>
> Es gibt für AVRs überhaupt gar keine keine libstdc++, und keine
> Exception handler, exceptions, oder irgend etwas in der Art. Damit ist
> die ganze Diskussion hier müßig.

Genau genommen ist die libsupc++ dafür zuständig.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.