Variation sur le thème du type punning: construction triviale sur place

9

Je sais que c'est un sujet assez courant, mais autant que l'UB typique est facile à trouver, je n'ai pas trouvé cette variante jusqu'à présent.

Donc, j'essaie d'introduire formellement des objets Pixel tout en évitant une copie réelle des données.

Est-ce valable?

struct Pixel {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    uint8_t alpha;
};

static_assert(std::is_trivial_v<Pixel>);

Pixel* promote(std::byte* data, std::size_t count)
{
    Pixel * const result = reinterpret_cast<Pixel*>(data);
    while (count-- > 0) {
        new (data) Pixel{
            std::to_integer<uint8_t>(data[0]),
            std::to_integer<uint8_t>(data[1]),
            std::to_integer<uint8_t>(data[2]),
            std::to_integer<uint8_t>(data[3])
        };
        data += sizeof(Pixel);
    }
    return result; // throw in a std::launder? I believe it is not mandatory here.
}

Modèle d'utilisation prévu, fortement simplifié:

std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data

Plus précisement:

  • Ce code a-t-il un comportement bien défini?
  • Si oui, est-il sûr d'utiliser le pointeur renvoyé?
  • Si oui, à quels autres Pixeltypes peut-il être étendu? (assouplissant la restriction is_trivial? pixel avec seulement 3 composants?).

Clang et gcc optimisent toute la boucle vers le néant, ce que je veux. Maintenant, je voudrais savoir si cela viole certaines règles C ++ ou non.

Lien Godbolt si vous voulez jouer avec.

(note: je n'ai pas tagué c ++ 17 malgré std::byte, parce que la question tient en utilisant char)

spectres
la source
2
Mais les Pixels contigus placés nouveaux ne sont toujours pas un tableau de Pixels.
Jarod42
1
@spectras Cela ne fait cependant pas un tableau. Vous avez juste un tas d'objets Pixel côte à côte. C'est différent d'un tableau.
NathanOliver
1
Alors non, où faites-vous pixels[some_index]ou *(pixels + something)? Ce serait UB.
NathanOliver
1
La section pertinente est ici et la phrase clé est si P pointe vers un élément de tableau i d'un objet de tableau x . Ici pixels(P) n'est pas un pointeur vers un objet tableau mais un pointeur vers un seul Pixel. Cela signifie que vous ne pouvez y accéder que pixels[0]légalement.
NathanOliver
3
Vous voulez lire wg21.link/P0593 .
ecatmur

Réponses:

3

C'est un comportement indéfini d'utiliser le résultat de promotecomme un tableau. Si nous regardons [expr.add] /4.2 nous avons

Sinon, si Ppointe vers un élément ide tableau d'un objet de tableaux avec des néléments ([dcl.array]), les expressions P + Jet J + P(où Ja la valeur j) pointent vers l'élément i+jde tableau (éventuellement hypothétique) de xif 0≤i+j≤net l'expression P - Jpointe vers ( éventuellement hypothétique) élément i−jde tableau de xif 0≤i−j≤n.

nous voyons qu'il nécessite que le pointeur pointe réellement vers un objet tableau. Cependant, vous n'avez pas réellement d'objet tableau. Vous avez un pointeur sur un single Pixelqui se trouve être Pixelssuivi par d' autres dans la mémoire contiguë. Cela signifie que le seul élément auquel vous pouvez réellement accéder est le premier élément. Essayer d'accéder à autre chose serait un comportement indéfini car vous avez dépassé la fin du domaine valide pour le pointeur.

NathanOliver
la source
Merci de l'avoir découvert si rapidement. Je vais faire un itérateur à la place, je suppose. En tant que sidenote, cela signifie également que &somevector[0] + 1c'est UB (enfin, je veux dire, utiliser le pointeur résultant serait).
spectras
@spectras Ce n'est pas grave. Vous pouvez toujours placer le pointeur sur un objet passé. Vous ne pouvez tout simplement pas déréférencer ce pointeur, même s'il existe un objet valide.
NathanOliver
Oui, j'ai modifié le commentaire pour être plus clair, je voulais dire déréférencer le pointeur résultant :) Merci d'avoir confirmé.
spectras
@spectras Aucun problème. Cette partie de C ++ peut être très difficile. Même si le matériel fera ce que nous voulons qu'il fasse, ce n'est pas vraiment ce à quoi servait le codage. Nous codons pour la machine abstraite C ++ et c'est une machine persnickety;) Espérons que P0593 sera adopté et cela deviendra beaucoup plus facile.
NathanOliver
1
@spectras Non, car un vecteur std est défini comme contenant un tableau et vous pouvez effectuer une arithmétique de pointeur entre les éléments du tableau. Il n'y a aucun moyen d'implémenter le vecteur std en C ++ lui-même, malheureusement, sans courir dans UB.
Yakk - Adam Nevraumont
1

Vous avez déjà une réponse concernant l'utilisation limitée du pointeur renvoyé, mais je tiens à ajouter que je pense également que vous devez std::laundermême pouvoir accéder au premier Pixel:

L'opération reinterpret_castest effectuée avant la création d'un Pixelobjet (en supposant que vous ne le faites pas dans getSomeImageData). Par conséquent reinterpret_cast, ne modifiera pas la valeur du pointeur. Le pointeur résultant pointera toujours vers le premier élément du std::bytetableau passé à la fonction.

Lorsque vous créez les Pixelobjets, ils vont être imbriqués dans le std::bytetableau et le std::bytetableau fournira un stockage pour les Pixelobjets.

Il existe des cas où la réutilisation du stockage provoque un pointeur vers l'ancien objet pour pointer automatiquement vers le nouvel objet. Mais ce n'est pas ce qui se passe ici, donc resultpointera toujours vers l' std::byteobjet, pas l' Pixelobjet. Je suppose que l'utiliser comme s'il pointait vers un Pixelobjet va techniquement être un comportement indéfini.

Je pense que cela est toujours valable, même si vous effectuez la reinterpret_castcréation après avoir créé l' Pixelobjet, car l' Pixelobjet et celui std::bytequi le stocke ne sont pas interchangeables avec un pointeur . Ainsi, même dans ce cas, le pointeur continuerait de pointer vers le std::byte, pas vers l' Pixelobjet.

Si vous avez obtenu le pointeur pour revenir du résultat de l'un des placement-new, alors tout devrait être correct, en ce qui concerne l'accès à cet Pixelobjet spécifique .


Vous devez également vous assurer que le std::bytepointeur est correctement aligné Pixelet que le tableau est vraiment assez grand. Pour autant que je me souvienne, la norme n'exige pas vraiment qu'elle Pixelait le même alignement std::byteou qu'elle n'ait pas de rembourrage.


De plus, rien de tout cela ne dépend de Pixelsa banalité ou de toute autre propriété de celui-ci. Tout se comporterait de la même manière tant que le std::bytetableau est de taille suffisante et convenablement aligné pour les Pixelobjets.

noyer
la source
Je pense que c'est exact. Même si la chose du tableau (de unimplementability de std::vector) n'a pas été un problème, vous auriez encore besoin de std::launderrésultat avant d' accéder à l' un des par emplacements newed Pixels. En ce moment, std::laundervoici UB, car les Pixels adjacents seraient accessibles à partir du pointeur blanchi .
Fureeish
@Fureeish Je ne sais pas pourquoi std::launderserait UB si postulé resultavant de revenir. Le adjacent Pixeln'est pas " accessible " par le pointeur blanchi selon ma compréhension de eel.is/c++draft/ptr.launder#4 . Et même si c'était le cas, je ne vois pas comment c'est UB, car tout le std::bytetableau d' origine est accessible à partir du pointeur d'origine.
noyer
Mais le suivant Pixelne sera pas accessible à partir du std::bytepointeur, mais il l'est à partir du launderpointeur ed. Je crois que c'est ici pertinent. Je suis cependant heureux d'être corrigé.
Fureeish
@Fureeish D'après ce que je peux dire, aucun des exemples donnés ne s'applique ici et la définition de l'exigence dit également la même chose que la norme. L'accessibilité est définie en termes d'octets de stockage, pas d'objets. L'octet occupé par le suivant Pixelme semble accessible à partir du pointeur d'origine, car le pointeur d'origine pointe vers un élément du std::bytetableau qui contient les octets constituant le stockage pour la Pixelfabrication du " ou dans le tableau immédiatement englobant dont Z est un l'élément "condition s'applique (où Zest Y, c'est-à-dire l' std::byteélément lui-même).
noyer le
Je pense que les octets de stockage que le prochain Pixeloccupe ne sont pas accessibles via le pointeur blanchi, car l' Pixelobjet pointé n'est pas un élément d'un objet de tableau et n'est pas non plus interchangeable avec un autre objet pertinent. Mais je pense aussi à ce détail std::launderpour la première fois dans cette profondeur. Je n'en suis pas sûr à 100% non plus.
noyer