Sei que esse é um assunto bastante comum, mas, por mais fácil que seja o UB típico, não encontrei essa variante até agora.
Então, estou tentando introduzir formalmente objetos Pixel, evitando uma cópia real dos dados.
Isso é válido?
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.
}
Padrão de uso esperado, altamente simplificado:
std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data
Mais especificamente:
- Esse código tem um comportamento bem definido?
- Se sim, torna seguro usar o ponteiro retornado?
- Em caso afirmativo, para que outros
Pixel
tipos ele pode ser estendido? (relaxando a restrição is_trivial? pixel com apenas 3 componentes?).
Tanto o clang quanto o gcc otimizam todo o loop para o nada, que é o que eu quero. Agora, eu gostaria de saber se isso viola algumas regras do C ++ ou não.
Godbolt link, se você quiser brincar com ele.
(nota: apesar de não ter marcado o c ++ 17 std::byte
, porque a pergunta continua sendo usada char
)
Pixel
s contíguos colocados como novos ainda não são uma matriz dePixel
s.pixels[some_index]
ou*(pixels + something)
? Isso seria UB.pixels
(P) não é um ponteiro para o objeto de matriz, mas um ponteiro para um únicoPixel
. Isso significa que você só pode acessarpixels[0]
legalmente.Respostas:
É um comportamento indefinido para usar o resultado
promote
como uma matriz. Se olharmos para [expr.add] / 4.2 , temosvemos que ele requer que o ponteiro aponte para um objeto de matriz. Na verdade, você não possui um objeto de matriz. Você tem um ponteiro para um único
Pixel
que, por acaso, possui outroPixels
na memória contígua. Isso significa que o único elemento que você pode acessar é o primeiro. Tentar acessar qualquer outra coisa seria um comportamento indefinido, porque você passou do final do domínio válido para o ponteiro.fonte
&somevector[0] + 1
é UB (bem, quero dizer, seria o uso do ponteiro resultante).Você já tem uma resposta sobre o uso limitado do ponteiro retornado, mas quero acrescentar que também acho que você precisa
std::launder
acessar o primeiroPixel
:O
reinterpret_cast
é feito antes de qualquerPixel
objeto é criado (supondo que você não fazê-lo emgetSomeImageData
). Portantoreinterpret_cast
, não alterará o valor do ponteiro. O ponteiro resultante ainda apontará para o primeiro elemento dastd::byte
matriz passado para a função.Quando você cria os
Pixel
objetos, eles serão aninhados nastd::byte
matriz e astd::byte
matriz fornecerá armazenamento para osPixel
objetos.Há casos em que a reutilização do armazenamento faz com que um ponteiro para o objeto antigo aponte automaticamente para o novo objeto. Mas não é isso que está acontecendo aqui, por
result
isso ainda apontará para ostd::byte
objeto, não para oPixel
objeto. Eu acho que usá-lo como se estivesse apontando para umPixel
objeto será tecnicamente um comportamento indefinido.Eu acho que isso ainda é válido, mesmo se você fizer o
reinterpret_cast
depois de criar oPixel
objeto, uma vez que oPixel
objeto e ostd::byte
que fornece armazenamento para ele não são intercambiáveis por ponteiro . Portanto, mesmo assim, o ponteiro continuaria apontando parastd::byte
oPixel
objeto , não para ele .Se você obteve o ponteiro para retornar do resultado de um dos novos posicionamentos, tudo deve ficar bem, no que diz respeito ao acesso a esse
Pixel
objeto específico .Além disso, você precisa garantir que o
std::byte
ponteiro esteja alinhado adequadamentePixel
e que a matriz seja realmente grande o suficiente. Tanto quanto me lembro, o padrão não exige realmente quePixel
tenha o mesmo alinhamentostd::byte
ou que não tenha preenchimento.Além disso, nada disso depende de
Pixel
ser trivial ou realmente qualquer outra propriedade dele. Tudo se comportaria da mesma maneira, desde que astd::byte
matriz tenha tamanho suficiente e esteja adequadamente alinhada para osPixel
objetos.fonte
std::vector
) não foi um problema, você ainda precisastd::launder
o resultado antes de acessar qualquer um dos por posicionamentonew
edPixel
s. A partir de agora,std::launder
aqui está o UB, já que osPixel
s adjacentes seriam alcançáveis a partir do ponteiro lavado .std::launder
que seria UB se aplicadoresult
antes de retornar. O adjacentePixel
não é " alcançável " através do ponteiro lavado, segundo meu entendimento de eel.is/c++draft/ptr.launder#4 . E mesmo assim, não vejo como é UB, porque toda astd::byte
matriz original é acessível a partir do ponteiro original.Pixel
não pode ser acessado pelostd::byte
ponteiro, mas é pelolaunder
ponteiro ed. Eu acredito que isso é relevante aqui. Estou feliz por ser corrigido, no entanto.Pixel
me parece acessível a partir do ponteiro original, porque o ponteiro original aponta para um elemento dastd::byte
matriz que contém os bytes que compõem o armazenamento para aPixel
criação da " ou dentro da matriz imediatamente adjacente da qual Z é um elemento "se aplica (ondeZ
estáY
, ou seja, ostd::byte
próprio elemento).Pixel
ocupa não são alcançáveis através do ponteiro lavado, porque oPixel
objeto apontado não é elemento de um objeto de matriz e também não é intercambiável por ponteiro com nenhum outro objeto relevante. Mas também estou pensando sobre esse detalhestd::launder
pela primeira vez nessa profundidade. Também não estou 100% certo disso.