Ich habe mir ein paar Helper Templates geschrieben um einen PIMPL ohne
eine Allokation per placement new in meinem Schnittstellen-Objekt zu
integrieren, läuft alles so wie es soll
jetzt möchte ich aber noch einen "einfach" Aufrufbare Template-Helper
haben der zur Kompilezeit das Alignment meines PIMPL Storage prüft
(Größe prüfe ich auch zur Kompilezeit aber das ist einfach), das zur
Laufzeit prüfen ist auch einfach aber das möchte ich vermeiden
mit cstddef und offsetof + MACRO geht das auch aber ich würde gerne
wissen ob es auch "sauberer" per Template geht
ich würde gerne
das
ich brauche blöderweise den Implementer type und die Size damit ich den
StorageMember deklarieren kann - leider verhindert das jede Form von
auto deduction - und da hänge ich in der Luft
Mit "auto" als Template-Argument-Typ gehts:
https://onlinegdb.com/BJWpauwrI
So wird "direkt" der Member-Pointer übergeben. Dann braucht man noch
zwei Hilfs-Funktionen um den Klassen-Typ und die Array-Größe aus dem
Member-Pointer-Typ zu extrahieren. Das geht so erst ab C++17, vorher
muss man da mit Wrapper-Funktionen und -Makros hantieren.
Das
ist allerdings Undefined Behaviour, denn dort werden Zeiger, die nicht
in das selbe Array zeigen, subtrahiert. Das geht in C++ nicht. Sollte
eigentlich Compiler-Fehler geben. Laut
https://en.cppreference.com/w/cpp/types/offsetof ist das normale
"offsetof" aber sowieso constexpr, funktioniert aber halt nicht mit
allen Klassen.
bist immer gleich da wenn man dich braucht :)
Niklas G. schrieb:> Das geht so erst ab C++17,
zum Glück kein Problem - auto in templates ist echt was sehr feines -
wie konnte man so lange ohne ...
>ist allerdings Undefined Behaviour, denn dort werden Zeiger, die nicht>in das selbe Array zeigen, subtrahiert. Das geht in C++ nicht. Sollte>eigentlich Compiler-Fehler geben.
Hast du vielleicht eine Non-UB-Template Lösung im Ärmel zum
rausschütteln hängen?
cppbert schrieb:> Hast du vielleicht eine Non-UB-Template Lösung im Ärmel zum> rausschütteln hängen?
Ich glaube das geht nicht... Vielleicht besser das ganze Problem
umgekehrt angehen - die Implementation von "PimplHider" nicht dem
Usercode überlassen, sondern in die Library verlagern, sodass du fix
dafür sorgen kannst, dass das Alignment immer stimmt, z.B. mit
std::aligned_storage oder "alignas" oder vielleicht auch mit
"std::optional".
Niklas G. schrieb:> cppbert schrieb:>> Hast du vielleicht eine Non-UB-Template Lösung im Ärmel zum>> rausschütteln hängen?>> Ich glaube das geht nicht... Vielleicht besser das ganze Problem> umgekehrt angehen - die Implementation von "PimplHider" nicht dem> Usercode überlassen, sondern in die Library verlagern, sodass du fix> dafür sorgen kannst, dass das Alignment immer stimmt, z.B. mit> std::aligned_storage oder "alignas" oder vielleicht auch mit> "std::optional".
du hast recht - weniger code, und dann muss ich nur alignof und sizeof
prüfen - dachte ich kann das auch mit nackten char-arrays machen
Danke für die Hilfe
cppbert schrieb:> du hast recht - weniger code, und dann muss ich nur alignof und sizeof> prüfen - dachte ich kann das auch mit nackten char-arrays machen
Jo. char-Arrays haben das Problem dass du sie nicht per constexpr direkt
mit einer Klasse initialisieren kannst, sondern nur per placement-new,
was aber nicht constexpr ist. Mit einer union (ggf. rekursiv
verschachtelt falls unterschiedliche Typen gewünscht) hingegen geht das.
Schau dir an wie die Standard-Bibliothek das bei std::variant macht...
cppbert schrieb:> Danke für die Hilfe
Gern :) mit ähnlichen Problemen hab ich mich auch schon rumgeschlagen
;-)
Niklas G. schrieb:> Jo. char-Arrays haben das Problem dass du sie nicht per constexpr direkt> mit einer Klasse initialisieren kannst, sondern nur per placement-new,> was aber nicht constexpr ist.
bitte was?
kann man einen PIMPL der inline liegt auch ganz ohne placement new
implementieren????
cppbert schrieb:> kann man einen PIMPL der inline liegt auch ganz ohne placement new> implementieren????
Äh, präzisiere doch mal was du genau machen willst, was der Unterschied
zu std::optional oder std::variant wäre und welchen Zweck ein "inline
pImpl" hat - klingt wie eine gerade Kurve. Placement new würde man nur
brauchen wenn man zwischendurch das Objekt zerstören und neu anlegen
möchte, ggf. mit anderer Klasse.
Niklas Gürtler schrieb:> Äh, präzisiere doch mal was du genau machen willst, was der Unterschied> zu std::optional oder std::variant wäre und welchen Zweck ein "inline> pImpl" hat
Niklas Gürtler == Niklas G.?
ich rede hier die ganze Zeit von einem ganz normaler PIMPL
Implementierung nur eben ohne Heap-Allokation (das Design in meinem
aktuellen Projekt ist mies und es gibt tausende solcher PIMPLs - es
lohnt sich den Speicher und die Allokation zu sparen...)
mit "inline" bezeichne ich das die Klasse die den Pimpl nutzt den
Speicher dafür in Form eines char[n] members anbietet - also gaaanz
simple
frontend.hpp
1
classFrontend
2
{
3
public:
4
booldoit();
5
private:
6
charm_pimpl_storage[n];
7
}
frontend.cpp
1
classPimpl
2
{
3
...
4
};
5
6
Frontend::Frontend()
7
{
8
// fuellt den m_pimpl_storage mit dem zu versteckenden Objekt
9
// und Frontend reicht dann Funktionalität an den Pimpl durch
10
}
11
12
boolFrontend::doit();
13
{
14
//an den echten code durchreichen
15
return(m_pimpl_storage).doit();// pesudo-code
16
}
mein Frontend hat absolut kein Wissen/Header von dem
Pimpl(Implementierungsdetail)
das läuft alles super, compiletime safe etc.
>std::optional oder std::variant
würde doch nur gehen wenn mein Frontend wissen über die
Implementierungsdetails hinter der Pimpl-Klasse hat, oder?
und wo bräuchte man kein placement new?
cppbert schrieb:> Niklas Gürtler == Niklas G.?
Ja natürlich :P
cppbert schrieb:> mein Frontend hat absolut kein Wissen/Header von dem> Pimpl(Implementierungsdetail)
Und wie ruft es dann das "doit" auf?
Eine Funktion der Form
kann ja nur funktionieren wenn der Header und Typ der inneren Klasse
("HiddenClass") bekannt ist. Und woher weiß das Frontend wie groß der
Speicher sein soll, wenn der Header des inneren Typs nicht bekannt ist?
Da die Größe von "Frontend" außerdem nicht fix ist und von der inneren
Klasse abhängt, kann "Frontend" auch nicht so flexibel benutzt werden;
der User-Code muss neukompiliert werden wenn sich die Größe von
"Frontend" ändert. Das ist ja der eigentliche Witz am echten pImpl - ein
Zeiger ist immer gleich groß, auch wenn sich die Größe des Ziels
ändert...
cppbert schrieb:> würde doch nur gehen wenn mein Frontend wissen über die> Implementierungsdetails hinter der Pimpl-Klasse hat, oder?
Ja.
cppbert schrieb:> und wo bräuchte man kein placement new?
Indem man die innere Klasse in eine union packt und diese
initialisiert... Aber auch das erfordert Kenntnis der inneren Klasse.
>return reinterpret_cast<HiddenClass&> (m_pimpl_storage).doit();
das war keine Frage :) - läuft ja alles
>kann ja nur funktionieren wenn der Header und Typ der inneren Klasse>("HiddenClass") bekannt ist. Und woher weiß das Frontend wie groß der>Speicher sein soll, wenn der Header des inneren Typs nicht bekannt ist?>Da die Größe von "Frontend" außerdem nicht fix ist und von der inneren>Klasse abhängt, kann "Frontend" auch nicht so flexibel benutzt werden;
alles bekannt und klar
in meinem Fall geht es nicht um Flexibilität sondern um
ein etwas blödes Design welches ich nicht noch weiter aufblähen
sondern reduzieren möchte
es gibt nur ca. 10 recht simple Frontends die so einen Pimpl brauchen
die sehr sehr sehr statisch sind aber !!tausendfach!! instanziert werden
die Größe und das Aligment wird im Frontend direkt hart eingetragen und
erst im Backend zur Kompilierzeit mit dem konkreten Typ verprüft
cppbert schrieb:> das war keine Frage :) - läuft ja alles
Ja aber wie? Inkludiert die Frontend.cpp die Header der inneren Klasse,
während die Frontend.hh das eben nicht tut, sodass man letztere ohne
Kenntnis der inneren Klasse nutzen kann?
cppbert schrieb:> die Größe und das Aligment wird im Frontend direkt hart eingetragen und> erst im Backend zur Kompilierzeit mit dem konkreten Typ verprüft
D.h. das Backend besteht aus Frontend.cpp und kennt auch die
Frontend.hh?
Das bedeutet ja dass der Konstruktor der Frontend sowieso nicht
constexpr sein kann, weil dieser ja dem User-Code nicht bekannt sein
kann. Von daher ist die ganze Überlegung eh hinfällig und du kannst
einfach placement-new machen.
Niklas G. schrieb:> Ja aber wie? Inkludiert die Frontend.cpp die Header der inneren Klasse,> während die Frontend.hh das eben nicht tut, sodass man letztere ohne> Kenntnis der inneren Klasse nutzen kann?
Ja
>D.h. das Backend besteht aus Frontend.cpp und kennt auch die>Frontend.hh?
Ja
>Das bedeutet ja dass der Konstruktor der Frontend sowieso nicht>constexpr sein kann, weil dieser ja dem User-Code nicht bekannt sein>kann. Von daher ist die ganze Überlegung eh hinfällig und du kannst>einfach placement-new machen.
Dachte ich mir
trotzdem Danke - immer wieder nett mit dir
Ich kann das ganze so noch nicht mal mit dem gcc9.2 auf einem x86-64
Rechner kompilieren.
error: size of array is not an integral constant-expression
char for_sizeof[(char *)&(TV::value.*MPtr) - (char *)&TV::value];
Ich würde dir aber aus praktischen Gründen von deinem Konzept abraten.
Um die dynamische Allokation zu umgehen, legst du die Größe des
Speichers im Header fest. Sizeof kannst du nicht verwenden, weil das
Objekt ja gerade nicht im Header definiert sein soll.
Im Prinzip musst du die Größe des Speichers manuell an die Größe des
Objektes anpassen, sonst gibt es eine Compilefehler. Aber woher kennst
du die Größe des Objektes?
- Ein Compiler könnte Elemente je nach Optimierung anders dimensionieren
oder padden.
- Der Compiler fügt Canaries hinzu, um zu erkennen, dass über das Ende
des Array geschrieben wurde.
- Eine virtuelle Methode hinzufügen oder erben und schon hat du einen
unsichtbaren Pointer auf den vtable, der verschieden groß sein kann.
- Je nach Prozessor könnte das Alignment anders sein und dadurch
weniger/mehr Lücken zwischen den Variablen entstehen.
- Du verwendest Elemente aus einer Bibliothek (z.B. std::string). Ändert
sich bei einem Update der Bibliothek die interne Implementierung, dann
passt deine Größe wieder nicht.
M.K. B. schrieb:> Ich kann das ganze so noch nicht mal mit dem gcc9.2 auf einem> x86-64> Rechner kompilieren.>> error: size of array is not an integral constant-expression> char for_sizeof[(char *)&(TV::value.*MPtr) - (char *)&TV::value];>> Ich würde dir aber aus praktischen Gründen von deinem Konzept abraten.>> Um die dynamische Allokation zu umgehen, legst du die Größe des> Speichers im Header fest. Sizeof kannst du nicht verwenden, weil das> Objekt ja gerade nicht im Header definiert sein soll.> Im Prinzip musst du die Größe des Speichers manuell an die Größe des> Objektes anpassen, sonst gibt es eine Compilefehler. Aber woher kennst> du die Größe des Objektes?>> - Ein Compiler könnte Elemente je nach Optimierung anders dimensionieren> oder padden.>> - Der Compiler fügt Canaries hinzu, um zu erkennen, dass über das Ende> des Array geschrieben wurde.>> - Eine virtuelle Methode hinzufügen oder erben und schon hat du einen> unsichtbaren Pointer auf den vtable, der verschieden groß sein kann.>> - Je nach Prozessor könnte das Alignment anders sein und dadurch> weniger/mehr Lücken zwischen den Variablen entstehen.>> - Du verwendest Elemente aus einer Bibliothek (z.B. std::string). Ändert> sich bei einem Update der Bibliothek die interne Implementierung, dann> passt deine Größe wieder nicht.
Mein Anwendungsfall ist speziell und die Grösse der versteckten Klasse
aendert sich so gut wie nie
Zur Kompilezeit pruefe ich ob Alignment und Size des Puffers mit der
verstecken Klasse übereinstimmen
Das mit den Canaries war mir nicht klar - aber wann kann man denn dann
sicher mit placement new arbeiten? Waere das dann nicht relativ haeufig
UB
cppbert schrieb:> Das mit den Canaries war mir nicht klar - aber wann kann man denn dann> sicher mit placement new arbeiten? Waere das dann nicht relativ haeufig> UB
Nein.
Du könntest deinen Speicher ja auch wie einen kleinen Heap verwalten.
Dann müsste die Größe nur größer oder gleich sein, als das Objekt. Der
Allocator würde dann einfach nur einen Speicherblock rausgeben und
danach nichts mehr. Beim new kennst du ja die richtige Größe. Du
könntest dann sogar dynamisch alignen, wenn es nicht passen sollte.
Ich hatte placement new mal für Ethernet Frames aus der Hardware
verwendet. Da habe ich die Structs mit pack pragmas auf dem von der
Hardware bereitgestellten Speicherbereich gemapped. Mit Memberfunktionen
konnt ich dann gut auf den Elementen arbeiten. In dem Fall brauchte ich
aber die ganzen pragmas, damit ich exakt das Layout der Hardware
reproduziere. Die hatten dann aber auch nur Integer als member und es
war auch nur bedingt auf andere Compiler portierbar und virtuelle
Funktionen gingen natürlich auch nicht.
Bei deiner Variant könnte es dir passieren, dass du die Größe je nach
Buildvariante (Release, Debug, Compiler ...) mit ifdef anders setzen
musst. Aus Erfahrung sind das die Stellen über die man sich beim Update
des Compilers ärgert.
Erzeugst du die Pimpl Objekte regelmäßig neu oder nur beim Start
einmalig? Dann könnte dir vielleicht auch ein schlanker Heap mit eigenem
Allocator helfen.
M.K. B. schrieb:> cppbert schrieb:>> Das mit den Canaries war mir nicht klar - aber wann kann man denn dann>> sicher mit placement new arbeiten? Waere das dann nicht relativ haeufig>> UB>> Nein.
Und warum sind moegliche Canaries in meinem Fall dann gefaehrlich?
Ist deine Erklärung schwammig oder mein Gehirn?
> Bei deiner Variant könnte es dir passieren, dass du die Größe je nach> Buildvariante (Release, Debug, Compiler ...) mit ifdef anders setzen> musst. Aus Erfahrung sind das die Stellen über die man sich beim Update> des Compilers ärgert.
Ist 100% klar - ich pruefe zur Kompilezeit und es ist eine klar
abgegrenze Menge an Klassen die sich so gut wie nie ändern
> Erzeugst du die Pimpl Objekte regelmäßig neu oder nur beim Start> einmalig? Dann könnte dir vielleicht auch ein schlanker Heap mit eigenem> Allocator helfen.
Ich erzeuge sehr sehr viele Objekte (nicht mein Design und ich darf nur
optimieren) davon und will die Allokationen oder ein Management
definitiv vermeiden und auch im Fall des eigenen Allokators verstehe ich
nicht warum da die Canaries nicht genau so gefaehrlich sind
cppbert schrieb:> Und warum sind moegliche Canaries in meinem Fall dann gefaehrlich?> Ist deine Erklärung schwammig oder mein Gehirn?
Ich meine nicht, dass es gefährlich ist. Wenn es bei dir compiliert,
dann funktioniert es.
Für mich wäre es nur ein Problem im Entwicklungsprozess.
Jemand verwendet ein Objekt aus einer Bibliothek, das z.B. im Debugbuild
mehr Prüfungen macht und dazu zusätzliche Member hat. Beim Bauen von
einer optimierten Release Variante passt dann die Größe nicht mehr.
Was würdest du dann als Größe für deinen Speicher angeben?
M.K. B. schrieb im Beitrag #617720
> Für mich wäre es nur ein Problem im Entwicklungsprozess.> Jemand verwendet ein Objekt aus einer Bibliothek, das z.B. im Debugbuild> mehr Prüfungen macht und dazu zusätzliche Member hat. Beim Bauen von> einer optimierten Release Variante passt dann die Größe nicht mehr.> Was würdest du dann als Größe für deinen Speicher angeben?
Das was bei x86,x64 DEBUG und RELEASE jeweils raus kommt im schlimmsten
Fall 4x alignment und 4x size: #ifdef,#elif,#else - aber auch nur weil
die Menge fix und ueberschaubar ist, sonst waere das sehr unpraktisch