std :: bit_cast avec std :: array

14

Dans son récent discours «Type punning en C ++ moderne», Timur Doumler a déclaré que std::bit_castcela ne peut pas être utilisé pour convertir un bit floaten un unsigned char[4]car les tableaux de style C ne peuvent pas être renvoyés d'une fonction. Nous devons utiliser std::memcpyou attendre C ++ 23 (ou version ultérieure) lorsque quelque chose comme reinterpret_cast<unsigned char*>(&f)[i]cela deviendra bien défini.

En C ++ 20, pouvons-nous utiliser un std::arrayavec std::bit_cast,

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

au lieu d'un tableau de style C pour obtenir des octets d'un float?

Evg
la source

Réponses:

15

Oui, cela fonctionne sur tous les grands compilateurs, et pour autant que je sache en regardant la norme, il est portable et garanti de fonctionner.

Tout d'abord, il std::array<unsigned char, sizeof(float)>est garanti qu'il s'agit d'un agrégat ( https://eel.is/c++draft/array#overview-2 ). Il s'ensuit qu'il contient exactement un sizeof(float)certain nombre de chars à l'intérieur (généralement sous la forme a char[], bien que la norme afaics n'impose pas cette implémentation particulière - mais il dit que les éléments doivent être contigus) et ne peut pas avoir de membres non statiques supplémentaires.

Il est donc trivialement copiable, et sa taille correspond également à celle de float.

Ces deux propriétés vous permettent de les bit_castséparer.

Timur Doumler
la source
3
Notez que cela struct X { unsigned char elems[5]; };satisfait la règle que vous citez. Il peut certainement être initialisé par liste avec jusqu'à 4 éléments. Il peut également être initialisé par liste avec 5 éléments. Je ne pense pas qu'un implémenteur de bibliothèque standard déteste suffisamment les gens pour le faire, mais je pense que c'est techniquement conforme.
Barry
Merci! - Barry, je ne pense pas que ce soit tout à fait vrai. La norme dit: "peut être initialisé par liste avec jusqu'à N éléments". Mon interprétation est que "jusqu'à" implique "pas plus que". Ce qui signifie que vous ne pouvez pas faire elems[5]. Et à ce stade, je ne vois pas comment vous pourriez vous retrouver avec un agrégat où sizeof(array<char, sizeof(T)>) != sizeof(T)?
Timur Doumler
Je crois que le but de la règle ( « un agrégat qui peut être initialisé liste ... ») est de permettre soit struct X { unsigned char c1, c2, c3, c4; };ou struct X { unsigned char elems[4]; };- si , tandis que les caractères doivent être les éléments de cet ensemble, ce qui leur permet d'être soit des éléments globaux directs ou des éléments d'un sous-agrégat unique.
Timur Doumler
2
@Timur "jusqu'à" n'implique pas "pas plus de". De la même manière que l'implication P -> Qn'implique rien sur le cas où!P
Barry
1
Même si l'agrégat ne contient rien d'autre qu'un tableau d'exactement 4 éléments, il n'y a aucune garantie que arraylui - même n'aura pas de remplissage. Les implémentations de celui-ci peuvent ne pas avoir de remplissage (et toutes les implémentations qui doivent l'être doivent être considérées comme dysfonctionnelles), mais il n'y a aucune garantie que arraycela ne le sera pas.
Nicol Bolas
6

La réponse acceptée est incorrecte car elle ne prend pas en compte les problèmes d'alignement et de remplissage.

Par [tableau] / 1-3 :

L'en-tête <array>définit un modèle de classe pour stocker des séquences d'objets de taille fixe. Un tableau est un conteneur contigu. Une instance de array<T, N>stocke des Néléments de type T, c'est size() == Ndonc un invariant.

Un tableau est un agrégat qui peut être initialisé par liste avec un maximum d' N éléments dont les types sont convertibles en T.

Un tableau répond à toutes les exigences d'un conteneur et d'un conteneur réversible ( [container.requirements]), sauf qu'un objet tableau construit par défaut n'est pas vide et que l'échange n'a pas une complexité constante. Un tableau répond à certaines des exigences d'un conteneur de séquence. Les descriptions sont fournies ici uniquement pour les opérations sur tableau qui ne sont pas décrites dans l'un de ces tableaux et pour les opérations où il existe des informations sémantiques supplémentaires.

La norme n'exige pas réellement std::arrayd'avoir exactement un membre de données publiques de type T[N], donc en théorie il est possible que sizeof(To) != sizeof(From)ou is_­trivially_­copyable_­v<To>.

Je serai cependant surpris si cela ne fonctionne pas dans la pratique.

LF
la source
2

Oui.

Selon l' article qui décrit le comportement de std::bit_cast, et sa mise en œuvre proposée dans la mesure où les deux types ont la même taille et sont copiables, la distribution devrait réussir.

Une implémentation simplifiée de std::bit_castdevrait ressembler à:

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

Depuis un flotteur (4 octets) et un tableau de unsigned charpar size_of(float)rapport à tous ceux affirme, le sous - jacent std::memcpysera exécuté. Par conséquent, chaque élément du tableau résultant sera un octet consécutif du flottant.

Afin de prouver ce comportement, j'ai écrit un petit exemple dans l'explorateur du compilateur que vous pouvez essayer ici: https://godbolt.org/z/4G21zS . Le flottant 5.0 est correctement stocké sous la forme d'un tableau d'octets ( Ox40a00000) qui correspond à la représentation hexadécimale de ce nombre flottant dans Big Endian .

Manuel Gil
la source
Êtes-vous sûr qu'il std::arrayest garanti de ne pas avoir de bits de rembourrage, etc.?
LF
1
Malheureusement, le simple fait que certains codes fonctionnent n'implique aucun UB. Par exemple, nous pouvons écrire auto bits = reinterpret_cast<std::array<unsigned char, sizeof(float)>&>(f)et obtenir exactement la même sortie. Cela prouve-t-il quelque chose?
Evg
@LF selon spécification: std::arraysatisfait aux exigences de ContiguiosContainer (depuis C ++ 17) .
Manuel Gil
1
@ManuelGil: std::vectorrépond également aux mêmes critères et ne peut évidemment pas être utilisé ici. Y a-t-il quelque chose qui nécessite de std::arraycontenir les éléments à l'intérieur de la classe (dans un champ), l'empêchant d'être un simple pointeur vers le tableau interne? (comme dans le vecteur, qui a également une taille, que le tableau ne nécessite pas d'avoir dans un champ)
firda
@firda L'exigence globale d'obliger std::arrayefficacement à stocker les éléments à l'intérieur, mais je m'inquiète des problèmes de mise en page.
LF