Variação no tema de punição de tipo: construção trivial no local

9

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 Pixeltipos 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)

espectras
fonte
2
Mas Pixels contíguos colocados como novos ainda não são uma matriz de Pixels.
Jarod42 14/01
11
Espectros @ Isso não faz uma matriz embora. Você apenas tem um monte de objetos Pixel próximos um do outro. Isso é diferente de uma matriz.
NathanOliver 14/01
11
Então não onde você faz pixels[some_index]ou *(pixels + something)? Isso seria UB.
NathanOliver 14/01
11
A seção relevante está aqui e a frase-chave é se P aponta para um elemento da matriz i de um objeto de matriz x . Aqui pixels(P) não é um ponteiro para o objeto de matriz, mas um ponteiro para um único Pixel. Isso significa que você só pode acessar pixels[0]legalmente.
NathanOliver 14/01
3
Você deseja ler wg21.link/P0593 .
ecatmur 14/01

Respostas:

3

É um comportamento indefinido para usar o resultado promotecomo uma matriz. Se olharmos para [expr.add] / 4.2 , temos

Caso contrário, se Papontar para um elemento ida matriz de um objeto da matrizx com nelementos ([dcl.array]), as expressões P + Je J + P(where Jtem o valor j) apontam para o elemento i+jda matriz (possivelmente hipotético) de xif 0≤i+j≤ne a expressão P - Japonta para ( possivelmente hipotético) elemento i−jde matriz de xif 0≤i−j≤n.

vemos 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 Pixelque, por acaso, possui outro Pixelsna 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.

NathanOliver
fonte
Obrigado por descobrir isso rápido. Eu vou fazer um iterador, eu acho. Como nota de rodapé, isso também significa que &somevector[0] + 1é UB (bem, quero dizer, seria o uso do ponteiro resultante).
spectras 14/01
@spectras Tudo bem. Você sempre pode obter o ponteiro para um objeto passado. Você simplesmente não pode desreferenciar esse ponteiro, mesmo se houver um objeto válido lá.
NathanOliver 14/01
Sim, editei o comentário para me tornar mais claro, quis dizer desreferenciar o ponteiro resultante :) Obrigado por confirmar.
spectras 14/01
@spectras Sem problemas. Essa parte do C ++ pode ser muito difícil. Mesmo que o hardware faça o que queremos, não é para isso que serve a codificação. Estamos codificando para a máquina abstrata C ++ e é uma máquina persnickety;) Esperemos que o P0593 seja adotado e isso se torne muito mais fácil.
NathanOliver 14/01
11
@spectras Não, porque um vetor std é definido como contendo uma matriz e você pode fazer aritmética de ponteiro entre os elementos da matriz. Infelizmente, não há como implementar o vetor std no próprio C ++, sem executar o UB.
Yakk - Adam Nevraumont 14/01
1

Você já tem uma resposta sobre o uso limitado do ponteiro retornado, mas quero acrescentar que também acho que você precisa std::launderacessar o primeiro Pixel:

O reinterpret_casté feito antes de qualquer Pixelobjeto é criado (supondo que você não fazê-lo em getSomeImageData). Portanto reinterpret_cast, não alterará o valor do ponteiro. O ponteiro resultante ainda apontará para o primeiro elemento da std::bytematriz passado para a função.

Quando você cria os Pixelobjetos, eles serão aninhados na std::bytematriz e a std::bytematriz fornecerá armazenamento para os Pixelobjetos.

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 resultisso ainda apontará para o std::byteobjeto, não para o Pixelobjeto. Eu acho que usá-lo como se estivesse apontando para um Pixelobjeto será tecnicamente um comportamento indefinido.

Eu acho que isso ainda é válido, mesmo se você fizer o reinterpret_castdepois de criar o Pixelobjeto, uma vez que o Pixelobjeto e o std::byteque fornece armazenamento para ele não são intercambiáveis ​​por ponteiro . Portanto, mesmo assim, o ponteiro continuaria apontando para std::byteo Pixelobjeto , 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 Pixelobjeto específico .


Além disso, você precisa garantir que o std::byteponteiro esteja alinhado adequadamente Pixele que a matriz seja realmente grande o suficiente. Tanto quanto me lembro, o padrão não exige realmente que Pixeltenha o mesmo alinhamento std::byteou que não tenha preenchimento.


Além disso, nada disso depende de Pixelser trivial ou realmente qualquer outra propriedade dele. Tudo se comportaria da mesma maneira, desde que a std::bytematriz tenha tamanho suficiente e esteja adequadamente alinhada para os Pixelobjetos.

noz
fonte
Eu acredito que está correto. Mesmo que a coisa array (unimplementability de std::vector) não foi um problema, você ainda precisa std::laundero resultado antes de acessar qualquer um dos por posicionamento newed Pixels. A partir de agora, std::launderaqui está o UB, já que os Pixels adjacentes seriam alcançáveis ​​a partir do ponteiro lavado .
Fureeish 14/01
@ Fureeish Não sei por std::launderque seria UB se aplicado resultantes de retornar. O adjacente Pixelnã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 a std::bytematriz original é acessível a partir do ponteiro original.
noz
Mas o próximo Pixelnão pode ser acessado pelo std::byteponteiro, mas é pelo launderponteiro ed. Eu acredito que isso é relevante aqui. Estou feliz por ser corrigido, no entanto.
Fureeish 14/01
@Fureeish Pelo que posso dizer, nenhum dos exemplos dados se aplica aqui e a definição do requisito também diz o mesmo que o padrão. A acessibilidade é definida em termos de bytes de armazenamento, não objetos. O byte ocupado pelo próximo Pixelme parece acessível a partir do ponteiro original, porque o ponteiro original aponta para um elemento da std::bytematriz que contém os bytes que compõem o armazenamento para a Pixelcriação da " ou dentro da matriz imediatamente adjacente da qual Z é um elemento "se aplica (onde Zestá Y, ou seja, o std::bytepróprio elemento).
walnut
Eu acho que os bytes de armazenamento que o próximo Pixelocupa não são alcançáveis ​​através do ponteiro lavado, porque o Pixelobjeto 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 detalhe std::launderpela primeira vez nessa profundidade. Também não estou 100% certo disso.
noz