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
Pixel
types 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
)
Pixel
s contigus placés nouveaux ne sont toujours pas un tableau dePixel
s.pixels[some_index]
ou*(pixels + something)
? Ce serait UB.pixels
(P) n'est pas un pointeur vers un objet tableau mais un pointeur vers un seulPixel
. Cela signifie que vous ne pouvez y accéder quepixels[0]
légalement.Réponses:
C'est un comportement indéfini d'utiliser le résultat de
promote
comme un tableau. Si nous regardons [expr.add] /4.2 nous avonsnous 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
Pixel
qui se trouve êtrePixels
suivi 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.la source
&somevector[0] + 1
c'est UB (enfin, je veux dire, utiliser le pointeur résultant serait).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::launder
même pouvoir accéder au premierPixel
:L'opération
reinterpret_cast
est effectuée avant la création d'unPixel
objet (en supposant que vous ne le faites pas dansgetSomeImageData
). Par conséquentreinterpret_cast
, ne modifiera pas la valeur du pointeur. Le pointeur résultant pointera toujours vers le premier élément dustd::byte
tableau passé à la fonction.Lorsque vous créez les
Pixel
objets, ils vont être imbriqués dans lestd::byte
tableau et lestd::byte
tableau fournira un stockage pour lesPixel
objets.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
result
pointera toujours vers l'std::byte
objet, pas l'Pixel
objet. Je suppose que l'utiliser comme s'il pointait vers unPixel
objet va techniquement être un comportement indéfini.Je pense que cela est toujours valable, même si vous effectuez la
reinterpret_cast
création après avoir créé l'Pixel
objet, car l'Pixel
objet et celuistd::byte
qui le stocke ne sont pas interchangeables avec un pointeur . Ainsi, même dans ce cas, le pointeur continuerait de pointer vers lestd::byte
, pas vers l'Pixel
objet.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
Pixel
objet spécifique .Vous devez également vous assurer que le
std::byte
pointeur est correctement alignéPixel
et que le tableau est vraiment assez grand. Pour autant que je me souvienne, la norme n'exige pas vraiment qu'ellePixel
ait le même alignementstd::byte
ou qu'elle n'ait pas de rembourrage.De plus, rien de tout cela ne dépend de
Pixel
sa banalité ou de toute autre propriété de celui-ci. Tout se comporterait de la même manière tant que lestd::byte
tableau est de taille suffisante et convenablement aligné pour lesPixel
objets.la source
std::vector
) n'a pas été un problème, vous auriez encore besoin destd::launder
résultat avant d' accéder à l' un des par emplacementsnew
edPixel
s. En ce moment,std::launder
voici UB, car lesPixel
s adjacents seraient accessibles à partir du pointeur blanchi .std::launder
serait UB si postuléresult
avant de revenir. Le adjacentPixel
n'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 lestd::byte
tableau d' origine est accessible à partir du pointeur d'origine.Pixel
ne sera pas accessible à partir dustd::byte
pointeur, mais il l'est à partir dulaunder
pointeur ed. Je crois que c'est ici pertinent. Je suis cependant heureux d'être corrigé.Pixel
me semble accessible à partir du pointeur d'origine, car le pointeur d'origine pointe vers un élément dustd::byte
tableau qui contient les octets constituant le stockage pour laPixel
fabrication du " ou dans le tableau immédiatement englobant dont Z est un l'élément "condition s'applique (oùZ
estY
, c'est-à-dire l'std::byte
élément lui-même).Pixel
occupe ne sont pas accessibles via le pointeur blanchi, car l'Pixel
objet 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étailstd::launder
pour la première fois dans cette profondeur. Je n'en suis pas sûr à 100% non plus.