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
classCounter{
4
private:
5
intcount;
6
7
public:
8
Counter():count(0){}// Constructor initializes count to 0
9
10
voidexec(){
11
count++;
12
}
13
14
intgetCount()const{
15
returncount;
16
}
17
};
18
19
classCounter_IF{
20
private:
21
CountercounterInstance;// Instance of Counter
22
23
public:
24
voidexec(){
25
counterInstance.exec();// Call exec on the Counter instance
26
}
27
28
intgetCount()const{
29
returncounterInstance.getCount();// Retrieve count from the Counter instance
30
}
31
};
32
33
intmain(){
34
Counter_IFcounters[5];// Array of 5 Counter_IF instances
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.
>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?
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
intlastHeapMemAddress()
9
{
10
externint*__brkval;
11
return(int)__brkval;
12
}
13
intheapStartMemAddress()
14
{
15
externint*__heap_start;
16
return(int)__heap_start;
17
}
18
19
intfreeRam(){
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.
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!
Christoph M. schrieb:> Mein Beispiel war etwas irreführendChristoph 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.
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.
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.
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
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:
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.
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.
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.
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.
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.
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.
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.
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.
>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.
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.
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.
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.
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.
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.
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.
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
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.
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:
/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
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.
> 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.
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:
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.
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.
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...
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.
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)?
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.
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.
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.
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?
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
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:
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.
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
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.
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.
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.
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.
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"...
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
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
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
voidtest(){
2
try{
3
foo();
4
}catch(interr){
5
puts("Error");
6
}
7
}
Man kann halt nur nichts damit machen weil es kein Unwinding gibt.
Theoretisch könnte man das implementieren...
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 handlerhttps://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.
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
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
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?
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.
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.
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.