Forum: PC-Programmierung C++17, alignment-check template Interface reduzierbar?


von cppbert (Gast)


Lesenswert?

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
1
    AlignCheck<PimplHider, sizeof(PimplHider::m_pimpl_storage), &PimplHider::m_pimpl_storage, Pimpl>();

auf sowas runter bringen
1
    AlignCheck<&PimplHider::m_pimpl_storage, Pimpl>();

Ist das möglich oder stosse ich da an C++ Ausdrucksgrenzen :)

Zum Online-Spielen:
https://onlinegdb.com/S10vMdwBI
1
#include <cstddef>
2
3
//von: https://thecppzoo.blogspot.com/2016/10/constexpr-offsetof-practical-way-to.html
4
namespace thecppzoo
5
{
6
//Frage: ist diese Implementierung frei von undefined behavior?
7
    
8
namespace detail {
9
10
    template<typename T> 
11
    struct declval_helper { static T value; };
12
13
    template<typename T, typename Z, Z T::*MPtr>
14
    struct offset_helper {
15
        using TV = declval_helper<T>;
16
        char for_sizeof[(char *)&(TV::value.*MPtr) - (char *)&TV::value];
17
    };
18
19
}
20
21
template<typename T, typename Z, Z T::*MPtr>
22
constexpr int offset_of() {
23
    return sizeof(detail::offset_helper<T, Z, MPtr>::for_sizeof);
24
}
25
}
26
27
template<typename Implementer, std::size_t Size, char (Implementer::*StorageMember)[Size], typename PimplType>
28
constexpr void AlignCheck()
29
{
30
    static_assert(Size == sizeof(PimplType), "wrong storage size");
31
    
32
    constexpr int offset = thecppzoo::offset_of<Implementer, char[Size], StorageMember>();
33
    constexpr size_t res = offset % alignof(PimplType);
34
    static_assert(!res, "storage alignment wrong");
35
}
36
37
// >>> PimplHider.hpp
38
39
class PimplHider
40
{
41
public:
42
    PimplHider();
43
private:
44
    //char x; // einkommentieren erzeugt ein AlignCheck-static_assert, dann muesste man z.B. auf m_pimple_storage alignas(4) anwenden
45
    char m_pimpl_storage[4]{};
46
};
47
48
// <<< end of PimplHider.hpp
49
50
// >>> PimpleHider.cpp
51
52
struct Pimpl
53
{
54
    int pimple_stuff{0};
55
};
56
57
PimplHider::PimplHider()
58
{
59
    // Funktioniert!
60
    AlignCheck<PimplHider, sizeof(PimplHider::m_pimpl_storage), &PimplHider::m_pimpl_storage, Pimpl>();
61
    
62
    //Fragen:
63
    
64
    // ist diese verkürzte Schnittstelle
65
    // irgendwie mit einem Template moeglich?
66
    //AlignCheck<&PimplHider::m_pimpl_storage, Pimpl>();
67
    
68
    // Anmerkung: Es definiert das der Storage immer ein char[n] sein muss
69
}
70
71
// <<< end of PimpleHider.cpp
72
73
int main()
74
{
75
    return 0;
76
}

Hat jemand eine Idee?

von cppbert (Gast)


Lesenswert?

stärker reduziert
1
template <typename Implementer, size_t Size, char(Implementer::*StorageMember)[Size]>
2
struct DeductStorageInfo
3
{
4
    using implementer_type = Implementer;
5
    static constexpr size_t size = Size;
6
    using member_ptrchar(Implementer::*StorageMember)[Size] = char(Implementer::*StorageMember)[Size]
7
};
8
9
using ds = DeductStorageInfo<PimplHider, sizeof(PimplHider::m_pimpl_storage), &PimplHider::m_pimpl_storage>;

könnte man diese Template darauf reduzieren
Storage ist immer ein char[Size]
1
using ds = DeductStorageInfo<&PimplHider::m_pimpl_storage>;

von cppbert (Gast)


Lesenswert?

cppbert schrieb:
> struct DeductStorageInfo
> {
>     using implementer_type = Implementer;
>     static constexpr size_t size = Size;
>     using member_ptrchar(Implementer::*StorageMember)[Size] =
> char(Implementer::*StorageMember)[Size]
> };

sorry
1
template <typename Implementer, size_t Size, char(Implementer::*StorageMember)[Size]>
2
struct DeductStorageInfo
3
{
4
    using implementer_type = Implementer;
5
    static constexpr size_t size = Size;
6
    using member_ptr = Implementer::*StorageMember;
7
};

von cppbert (Gast)


Lesenswert?

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

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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
1
char for_sizeof[(char *)&(TV::value.*MPtr) - (char *)&TV::value];

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.

von cppbert (Gast)


Lesenswert?

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?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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".

von cppbert (Gast)


Lesenswert?

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

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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 
;-)

von cppbert (Gast)


Lesenswert?

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????

von Niklas Gürtler (Gast)


Lesenswert?

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.

von cppbert (Gast)


Lesenswert?

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
class Frontend
2
{
3
public:
4
  bool doit();
5
private:
6
  char m_pimpl_storage[n];
7
}

frontend.cpp
1
class Pimpl
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
bool Frontend::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?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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
1
bool Frontend::doit();
2
{
3
  //an den echten code durchreichen
4
  return reinterpret_cast<HiddenClass&> (m_pimpl_storage).doit();
5
}

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.

von cppbert (Gast)


Lesenswert?

>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

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von cppbert (Gast)


Lesenswert?

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

von M.K. B. (mkbit)


Lesenswert?

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.

von cppbert (Gast)


Lesenswert?

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

von M.K. B. (mkbit)


Lesenswert?

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.

von cppbert (Gast)


Lesenswert?

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

von M.K. B. (mkbit)


Lesenswert?

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?

von cppbert (Gast)


Lesenswert?

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

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.