Forum: Compiler & IDEs C++ template mit typabhängiger Behandlung


von Johannes S. (Gast)


Lesenswert?

Frage vorweg: kann ich in einem Template Klassen und einfache Typen 
unterschiedlich behandeln?

Ich habe in Mbed eine Mail Klasse, diese kombiniert eine Queue mit einem 
Memorypool. Zweck ist z.B. Daten aus einer ISR oder einem Sendethread in 
einen Verarbeitungsthread zu transportieren, siehe auch das Beispiel am 
Ende der Doku auf:
https://os.mbed.com/docs/mbed-os/v6.9/apis/mail.html

Das funktioniert auch solange T z.B. eine Datenstruktur wie im Beispiel 
ist. Wenn T eine Klasse ist, dann kompiliert das zwar fehlerfrei, aber 
der Konstruktor von T wird nicht aufgerufen und Classmember sind nicht 
initialisert.
Mail benutzt einen Memorypool für Elemente gleicher Größe, im 
Konstruktor sieht man das nur Speicher geholt wird aber keine 
Konstruktoren für die Elemente aufgerufen werden:
1
template<typename T, uint32_t pool_sz>
2
class MemoryPool : private mbed::NonCopyable<MemoryPool<T, pool_sz> > {
3
    static_assert(pool_sz > 0, "Invalid memory pool size. Must be greater than 0.");
4
public:
5
    /** Create and Initialize a memory pool.
6
     *
7
     * @note You cannot call this function from ISR context.
8
    */
9
    MemoryPool()
10
    {
11
        memset(_pool_mem, 0, sizeof(_pool_mem));
12
        osMemoryPoolAttr_t attr = { 0 };
13
        attr.mp_mem = _pool_mem;
14
        attr.mp_size = sizeof(_pool_mem);
15
        attr.cb_mem = &_obj_mem;
16
        attr.cb_size = sizeof(_obj_mem);
17
        _id = osMemoryPoolNew(pool_sz, sizeof(T), &attr);
18
        MBED_ASSERT(_id);
19
    }

Hier könnte ich einbauen das Elemente initialisiert werden:
1
    for (uint32_t i=0; i<pool_sz; i++) {
2
        T* pT = new T;
3
        //pT in Array speichern
4
    }

so ist das natürlich nicht gut weil da nochmal Speicher geholt würde. 
Ich müsste ein new haben dem ich eine Speicheradresse mitgeben kann oder 
den Konstruktor explizit für das noch nicht initialisierte Objekt 
aufrufen können. Würde mir etwas aus der C++ Standard Library helfen?
Und kann man im template die Konstruktoren nur für Klassen aufrufen und 
tote Strukturen einfach so allozieren wie es der Code jetzt macht?

Edit:
std::construct_at() habe ich gefunden, das ist mit C++20 aber noch zu 
modern.

von Zombie (Gast)


Lesenswert?

Ich glaube, was du suchst ist "placement new".
1
T* pT = new(addr) T(); // Constructor aufrufen (ohne Allokation)

oder auch nur:
1
new(addr) T(); // Constructor aufrufen (ohne Allokation)

und dann zum Schluss:
1
tptr->~T(); // Destructor aufrufen (ohne Deallokation)

von Rolf M. (rmagnus)


Lesenswert?

Ja, placement new und expliziter Destruktor-Aufruf passen genau zu 
dieser Beschreibung.

von Johannes S. (Gast)


Lesenswert?

Ok, danke, schaue ich mir gleich an.

von Wilhelm M. (wimalopaan)


Lesenswert?

Johannes S. schrieb:
> Frage vorweg: kann ich in einem Template Klassen und einfache Typen
> unterschiedlich behandeln?

Ja, auch das geht: nennt sich old-school type-trait oder etwas moderner 
eine Meta-Funktion. In diesem Fall: std::is_fundamental<>

von Johannes S. (Gast)


Lesenswert?

Das placement new hat funktioniert, es hat aber trotzdem wieder geknallt 
:(
Der Memorypool vom RTX verwaltet seine freien Blocks in den _pool_mem 
Daten. Die Blockzeiger überschreibe ich wenn ich den Speicher mit den 
Konstruktoren manipuliere. Da müsste ich die blocksize um einen Pointer 
vergrößern,  das scheint mir aber auch fragil zu sein. Alternative wäre 
wohl eine std::list.

Die type traits habe ich auch entdeckt, damit könnte man zumindest in 
der Mail Klasse einen Fehler werfen wenn die mit einer class/struct 
benutzt wird.

von Rolf M. (rmagnus)


Lesenswert?

Johannes S. schrieb:
> Der Memorypool vom RTX verwaltet seine freien Blocks in den _pool_mem
> Daten. Die Blockzeiger überschreibe ich wenn ich den Speicher mit den
> Konstruktoren manipuliere.

Warum?

> Da müsste ich die blocksize um einen Pointer vergrößern,  das scheint mir aber
> auch fragil zu sein. Alternative wäre wohl eine std::list.

Tut mir leid, irgendwie verstehe ich nicht, was du meinst.

von Johannes S. (Gast)


Lesenswert?

das ist der MemoryPool wie im ersten Post:
https://github.com/ARMmbed/mbed-os/blob/master/rtos/include/rtos/MemoryPool.h

da wird in der Initialisierung osMemoryPoolNew() aufgerufen, und das 
legt eine verkettete Liste in den _pool_mem Daten an, also an die ersten 
4 Byte wird ein Pointer auf den nächsten Block geschrieben.
https://github.com/ARMmbed/mbed-os/blob/73896715e6202e187ba624fc3853299e73390f19/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_mempool.c#L66-L73

und diese Initialisierung mache ich platt wenn ich danach den Speicher 
mit den Objektdaten überschreibe. Ist also nicht richtig C++ kompatibel 
bzw. man sollte den Speicher wohl in Ruhe lassen.
Wenn dieses Verhalten so bleibt könnte ich den Speicher pro Block auf 
sizeof(void*)+sizeof(T) vergrößern und mein Objekt damit hinter den 
Zeiger legen.

Mein Anwendungsbeispiel hatte ich hier angehängt:
Beitrag "Re: Einstieg(scontroller) in Ethernet Kommunikation mit STM32"
Da hatte ich mich über die Hardfaults gewundert und die MidiMessage so 
geändert das die auch ohne Initialisierung funktioniert. Was aber nur 
ein kurieren von Symptomen war. Eine Templateklasse sollte schon mit 
allen Typen funktionieren oder einen Fehler werfen wenn sie nicht 
geeignet ist.

von Rolf M. (rmagnus)


Lesenswert?

Johannes S. schrieb:
> da wird in der Initialisierung osMemoryPoolNew() aufgerufen, und das
> legt eine verkettete Liste in den _pool_mem Daten an, also an die ersten
> 4 Byte wird ein Pointer auf den nächsten Block geschrieben.

Ok, aber das ist doch ein Implementierungsdetail des Memory-Pools. Warum 
kommst du damit überhaupt in Berührung? osMemoryPoolAlloc() bzw. 
try_alloc() gibt dir nachher einen Zeiger auf einen Speicherblock der 
Größe sizeof(T) zurück, den du frei nach belieben beschreiben kannst. 
Dieser Pool-interne Zeiger auf den nächsten Block ist außerhalb davon. 
Sofern dein Konstruktor keinen Fehler hat, sollte der dort also nie was 
hinschreiben.

von Johannes S. (Gast)


Lesenswert?

Rolf M. schrieb:
> Johannes S. schrieb:
> Ok, aber das ist doch ein Implementierungsdetail des Memory-Pools. Warum
> kommst du damit überhaupt in Berührung? osMemoryPoolAlloc() bzw.
> try_alloc() gibt dir nachher einen Zeiger auf einen Speicherblock der
> Größe sizeof(T) zurück, den du frei nach belieben beschreiben kannst.

ja, das ist richtig. Es funktioniert ja auch, aber nur solange ich keine 
Objekte als Mailtype habe. Es gibt Tests für Mail und Memorypool, die 
verwenden aber nur Strukturen wie z.B.:
1
typedef struct {
2
    uint16_t data;
3
    uint8_t thread_id;
4
} mail_t;
5
6
Mail<mail_t, 64> mailbox_struct;

In diesem Fall wird ein Block mit try_alloc() geholt und beschrieben, 
ok. Vor dem ausbuchen mit alloc wird der interne Zeiger in eine 
Verwaltungsstruktur gerettet und ich kann mit dem Speicher machen was 
ich will. Beim Zurückgeben mit free() wird wieder ein Zeiger 
eingetragen.

Jetzt kommt Johannes und möchte ein Objekt Message als 
Mailboxtyp/Memorypooltyp benutzen. Die Message kann größer sein und holt 
sich im Konstruktor Speicher vom Heap:
1
classe Message {
2
    Message::Message() { 
3
        data = new uint8_t[256];
4
        length = 0;
5
    };
6
7
    Message::from_raw(uint8_t* source, uint32_t len) {
8
        for (uint8_t i=0; i<len; i++) {
9
            data[i] = source[i];
10
    };
11
12
    uint8_t *data;
13
    uint16_t length;
14
}
15
16
Mail<Message, 64> mailbox_objects;

Der Compiler versteht mich, alles gut. Aber jetzt liefert mir 
try_alloc() ein nicht initialisiertes Message Objekt. Nachträglich den 
Konstruktor aufrufen ist nicht gut, new im ISR Kontext knallt und da 
möchte man so eine teure Operation auch nicht haben.
Also im oben gezeigten MemoryPool Konstruktor den gesamten Pool mit dem 
placement new behandeln und alle Objekte sind initialisiert. Aber das 
beißt sich jetzt mit der Initialsierung die osMemoryPoolNew() 
durchgeführt hat weil die Daten implizit auch die Verwaltung enthalten.
Man könnte die Verwaltungszeiger in ein extra Array legen, das braucht 
dann aber mehr Speicher wenn die Blöcke keinen Konstruktor brauchen. Und 
die RTX Quellen möchte ich nicht verändern, das ist eine 3rd Party 
Komponente die beim Update ja wieder modifiziert werden müsste. Sowas 
möchte man als PR sicher nicht haben.
Also bleibt eigentlich nur für den Fall 'T hat Konstruktor' die Variante 
die Blockgröße um den Platz für den Verwaltungszeiger zu vergrößern 
damit dieser und die initialisierten Objektdaten überleben.

Ich hoffe das war jetzt nicht zu kompliziert. Das man initialisierte 
Objekte von 'T* try_alloc()' bekommt halte ich für RAII konform und 
wichtig. Alternativ könnte man einen Fehler melden wenn Mail<> einen 
nicht unterstützten Typ mit Konstruktor bekommt (kann ja auch eine 
struct sein). Aber für den Fall müsste man das Rad neu erfinden, 
Mail/MemoryPool bietet ja schon eine Menge incl. Timeout Behandlung.

So sieht mein MemoryPool Konstruktor mit placement new jetzt aus, aber 
schreddert jetzt wie gesagt die Verzeigerung der freigen Blöcke:
1
    MemoryPool()
2
    {
3
        memset(_pool_mem, 0, sizeof(_pool_mem));
4
        osMemoryPoolAttr_t attr = { 0 };
5
        attr.mp_mem = _pool_mem;
6
        attr.mp_size = sizeof(_pool_mem);
7
        attr.cb_mem = &_obj_mem;
8
        attr.cb_size = sizeof(_obj_mem);
9
        _id = osMemoryPoolNew(pool_sz, sizeof(T), &attr);
10
        MBED_ASSERT(_id);
11
12
        // initialize elements
13
        volatile uint32_t block_size = osMemoryPoolGetBlockSize (_id);
14
        for (uint32_t i=0; i<pool_sz; i++) {
15
            void* addr = &_pool_mem[block_size * i];
16
            // use displacement new to call constructor without memory allocaction
17
            new (addr) T();
18
        }
19
    }

von Rolf M. (rmagnus)


Lesenswert?

Johannes S. schrieb:
> Nachträglich den Konstruktor aufrufen ist nicht gut, new im ISR Kontext
> knallt und da möchte man so eine teure Operation auch nicht haben.

Ok, so weit klar.

> Also im oben gezeigten MemoryPool Konstruktor den gesamten Pool mit dem
> placement new behandeln und alle Objekte sind initialisiert.

Aber dort sind das ja in dem Sinne noch keine Objekte, sondern einfach 
nur ein Block "roher" Speicher, der intern für den MemoryPool ist. Was 
du darin als Message-Objekt initialisieren kannst, weißt du erst, wenn 
du try_allocate() aufgerufen und den entsprechenden Zeiger 
zurückbekommen hast, der auf den für dein Message-Objekt vorgesehenen 
Speicherblock zeigt.

> Aber das beißt sich jetzt mit der Initialsierung die osMemoryPoolNew()
> durchgeführt hat weil die Daten implizit auch die Verwaltung enthalten.

Ja, ok, jetzt verstehe ich. Wie gesagt ist das ja auch kein Speicher, in 
dem du einfach frei rumschreiben solltest.

> Man könnte die Verwaltungszeiger in ein extra Array legen, das braucht
> dann aber mehr Speicher wenn die Blöcke keinen Konstruktor brauchen.

Wenn du kein Problem damit hast, die Objekte alle gleich von Anfang an 
initialisiert zu haben, warum hast du dann überhaupt einen Memorypool 
und nicht einfach ein Array aus Message-Objekten?

> Also bleibt eigentlich nur für den Fall 'T hat Konstruktor' die Variante
> die Blockgröße um den Platz für den Verwaltungszeiger zu vergrößern
> damit dieser und die initialisierten Objektdaten überleben.

Damit machst du dich halt vom internen Aufbau der verwendeten Komponente 
abhängig. So richtig sauber finde ich das nicht.

von Johannes S. (Gast)


Lesenswert?

Rolf M. schrieb:
> Wenn du kein Problem damit hast, die Objekte alle gleich von Anfang an
> initialisiert zu haben, warum hast du dann überhaupt einen Memorypool
> und nicht einfach ein Array aus Message-Objekten?

Der Memory Pool hat ja zusätzlich die Verwaltung von benutzten/freien 
Blöcken, das ist schonmal praktisch und mit der Verzeigerung auch 
schnell und effizient. Das habe ich erst so gesehen als ich den Fehler 
gesucht habe, da war erst mal der sportliche Ehrgeiz zu verstehen was da 
passiert. Als use case passte das Beispiel 'Daten in thread/ISR erzeugen 
und in einem anderen verarbeiten' ja auch zunächst.
Der MemoryPool hat ja noch die Eigenschaft das mehrere Blöcke 
gleichzeitig ausgebucht sein dürfen und in beliebiger Reihenfolge 
zurückgegeben werden dürfen. Für den vereinfachten Fall in der Anwendung 
mit Fifo und sequentieller Verarbeitung sind wir ja beim beliebten 
Circular Buffer. Die fertigen RTOS Komponenten haben da noch zusätzlich 
die EventFlags und blockierendes Warten ohne Polling ist damit einfach.

Rolf M. schrieb:
> Damit machst du dich halt vom internen Aufbau der verwendeten Komponente
> abhängig. So richtig sauber finde ich das nicht.

ja, dünnes Eis. Wobei dieser MemoryPool sich vermutlich nicht mehr 
verändern wird, aber mal sehen was die Mbed Mädels und Jungs sagen.

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.