std :: bit_cast mit std :: array

14

In seinem kürzlich gehaltenen Vortrag "Type punning in modernem C ++" sagte Timur Doumler , dass std::bit_castdies nicht zum Bit-Casting floatin ein verwendet werden kann, unsigned char[4]da Arrays im C-Stil nicht von einer Funktion zurückgegeben werden können. Wir sollten entweder std::memcpyC ++ 23 (oder später) verwenden oder warten, bis etwas wie reinterpret_cast<unsigned char*>(&f)[i]gut definiert wird.

Können wir in C ++ 20 ein std::arraymit verwenden std::bit_cast,

float f = /* some value */;
auto bits = std::bit_cast<std::array<unsigned char, sizeof(float)>>(f);

anstelle eines C-artigen Arrays, um Bytes eines float?

Evg
quelle

Antworten:

15

Ja, dies funktioniert auf allen gängigen Compilern. Soweit ich den Standard beurteilen kann, ist es portabel und funktioniert garantiert.

Zunächst std::array<unsigned char, sizeof(float)>wird garantiert, dass es sich um ein Aggregat handelt ( https://eel.is/c++draft/array#overview-2 ). Daraus folgt, dass es genau eine sizeof(float)Anzahl von chars enthält (normalerweise als char[], obwohl der Standard diese spezielle Implementierung nicht vorschreibt - aber es heißt, dass die Elemente zusammenhängend sein müssen) und keine zusätzlichen nicht statischen Elemente haben kann.

Es ist daher trivial kopierbar und seine Größe entspricht auch der von float.

Mit diesen beiden Eigenschaften können Sie bit_castzwischen ihnen wechseln.

Timur Doumler
quelle
3
Beachten Sie, dass dies struct X { unsigned char elems[5]; };der von Ihnen angegebenen Regel entspricht. Es kann sicherlich mit bis zu 4 Elementen listinitialisiert werden. Es kann auch mit 5 Elementen listeninitialisiert werden. Ich glaube nicht, dass ein Standard-Bibliotheksimplementierer die Leute genug hasst, um dies tatsächlich zu tun, aber ich denke, es ist technisch konform.
Barry
Vielen Dank! - Barry, ich denke nicht, dass das ganz richtig ist. Der Standard sagt: "Kann mit bis zu N Elementen listinitialisiert werden". Meine Interpretation ist, dass "bis zu" "nicht mehr als" impliziert. Was bedeutet, dass Sie nicht tun können elems[5]. Und an diesem Punkt kann ich nicht sehen, wie Sie mit einem Aggregat enden könnten, wo sizeof(array<char, sizeof(T)>) != sizeof(T)?
Timur Doumler
Ich glaube, der Zweck der Regel ("ein Aggregat, das listeninitialisiert werden kann ...") besteht darin, entweder struct X { unsigned char c1, c2, c3, c4; };oder struct X { unsigned char elems[4]; };- zuzulassen , während die Zeichen die Elemente dieses Aggregats sein müssen, können sie entweder direkte Aggregatelemente sein oder Elemente eines einzelnen Unteraggregats.
Timur Doumler
2
@ Timur "bis zu" bedeutet nicht "nicht mehr als". In der gleichen Weise, wie die Implikation P -> Qnichts über den Fall impliziert, in dem!P
Barry
1
Selbst wenn das Aggregat nur ein Array von genau 4 Elementen enthält, gibt es keine Garantie dafür, dass es arrayselbst keine Auffüllung hat. Implementierungen davon haben möglicherweise keine Auffüllung (und alle Implementierungen, die dies tun, sollten als dysfunktional angesehen werden), aber es gibt keine Garantie dafür, dass dies nicht der Fall ist array.
Nicol Bolas
6

Die akzeptierte Antwort ist falsch, da Ausrichtungs- und Auffüllprobleme nicht berücksichtigt werden.

Per [Array] / 1-3 :

Der Header <array>definiert eine Klassenvorlage zum Speichern von Objektsequenzen fester Größe. Ein Array ist ein zusammenhängender Container. Eine Instanz array<T, N>speichert NElemente vom Typ T, sodass dies size() == Neine Invariante ist.

Ein Array ist ein Aggregat, das mit bis zu N Elementen, deren Typen in konvertierbar sind, listinitialisiert werden kann T.

Ein Array erfüllt alle Anforderungen eines Containers und eines reversiblen Containers ( [container.requirements]), mit der Ausnahme, dass ein standardmäßig erstelltes Array-Objekt nicht leer ist und der Swap keine konstante Komplexität aufweist. Ein Array erfüllt einige der Anforderungen eines Sequenzcontainers. Beschreibungen werden hier nur für Operationen an Arrays bereitgestellt, die nicht in einer dieser Tabellen beschrieben sind, und für Operationen, bei denen zusätzliche semantische Informationen vorhanden sind.

Der Standard verlangt eigentlich nicht std::array genau ein öffentliches Datenelement vom Typ T[N], daher ist es theoretisch möglich, dass sizeof(To) != sizeof(From)oder is_­trivially_­copyable_­v<To>.

Ich werde überrascht sein, wenn dies in der Praxis nicht funktioniert.

LF
quelle
2

Ja.

Nach dem Papier , das das Verhalten von std::bit_castund seine vorgeschlagene Implementierung beschreibt , sofern beide Typen dieselbe Größe haben und trivial kopierbar sind, sollte die Besetzung erfolgreich sein.

Eine vereinfachte Implementierung von std::bit_castsollte ungefähr so ​​aussehen:

template <class Dest, class Source>
inline Dest bit_cast(Source const &source) {
    static_assert(sizeof(Dest) == sizeof(Source));
    static_assert(std::is_trivially_copyable<Dest>::value);
    static_assert(std::is_trivially_copyable<Source>::value);

    Dest dest;
    std::memcpy(&dest, &source, sizeof(dest));
    return dest;
}

Da ein float (4 Bytes) und ein Array von unsigned charwithsize_of(float) Bezug auf alle diese Asserts den Basiswertstd::memcpy wird durchgeführt. Daher ist jedes Element in dem resultierenden Array ein aufeinanderfolgendes Byte des Floats.

Um dieses Verhalten zu beweisen, habe ich im Compiler Explorer ein kleines Beispiel geschrieben, das Sie hier ausprobieren können: https://godbolt.org/z/4G21zS . Der float 5.0 wird ordnungsgemäß als Array von bytes ( Ox40a00000) gespeichert , das der hexadezimalen Darstellung dieser float-Zahl in Big Endian entspricht .

Manuel Gil
quelle
Sind Sie sicher, dass std::arraygarantiert keine Füllbits usw. vorhanden sind?
LF
1
Leider bedeutet die bloße Tatsache, dass ein Teil des Codes funktioniert, keine UB. Zum Beispiel können wir schreiben auto bits = reinterpret_cast<std::array<unsigned char, sizeof(float)>&>(f)und genau die gleiche Ausgabe erhalten. Beweist es etwas?
Evg
@LF gemäß Spezifikation: Erfüllt std::arraydie Anforderungen von ContiguiosContainer (seit C ++ 17) .
Manuel Gil
1
@ManuelGil: std::vectorerfüllt auch die gleichen Kriterien und kann hier offensichtlich nicht verwendet werden. Gibt es etwas, das std::arraydie Elemente in der Klasse (in einem Feld) enthalten muss, um zu verhindern, dass sie ein einfacher Zeiger auf das innere Array sind? (wie in Vektor, der auch eine Größe hat, die Array nicht in einem Feld haben muss)
Firda
@firda Die Gesamtanforderung von std::arrayerfordert effektiv, dass die Elemente darin gespeichert werden, aber ich mache mir Sorgen über Layoutprobleme.
LF