O que impede a sobreposição de membros adjacentes nas classes?

12

Considere os três seguintes struct:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Nas plataformas típicas, com int4 bytes, os tamanhos, alinhamentos e preenchimento total 1 são os seguintes:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Não há sobreposição entre o armazenamento dos membros blube blob, mesmo que o tamanho 1 blobpossa, em princípio, "encaixar" no preenchimento de blub.

O C ++ 20 apresenta o no_unique_addressatributo, que permite que membros vazios adjacentes compartilhem o mesmo endereço. Também permite explicitamente o cenário descrito acima do uso de preenchimento de um membro para armazenar outro. De cppreference (ênfase minha):

Indica que esse membro de dados não precisa ter um endereço distinto de todos os outros membros de dados não estáticos de sua classe. Isso significa que se o membro tiver um tipo vazio (por exemplo, Alocador sem estado), o compilador pode otimizá-lo para não ocupar espaço, como se fosse uma base vazia. Se o membro não estiver vazio, qualquer preenchimento de cauda nele também poderá ser reutilizado para armazenar outros membros de dados.

De fato, se usarmos esse atributo em blub b0, o tamanho de blacai para 8, de modo que ele blobé realmente armazenado no blub como visto no godbolt .

Finalmente, chegamos à minha pergunta:

Que texto nos padrões (C ++ 11 a C ++ 20) impede essa sobreposição sem no_unique_address, para objetos que não são trivialmente copiáveis?

Eu preciso excluir objetos trivialmente copiáveis ​​(TC) do acima exposto, porque para objetos TC, é permitido passar std::memcpyde um objeto para outro, incluindo subobjetos de membros, e se o armazenamento fosse sobreposto, isso seria interrompido (porque todo ou parte do armazenamento para o membro adjacente seria substituído) 2 .


1 Calculamos o preenchimento simplesmente como a diferença entre o tamanho da estrutura e o tamanho de todos os seus membros constituintes, recursivamente.

2 É por isso que tenho construtores de cópia definidos: tornar blube blobnão trivialmente copiáveis .

BeeOnRope
fonte
Ainda não pesquisei, mas estou adivinhando a regra "como se". Se não houver diferença observável (um termo com significado muito específico) para a máquina abstrata (que é contra a qual seu código é compilado), o compilador poderá alterar o código da maneira que desejar.
Jesper Juhl 22/01
Tenho certeza de que isso é uma
bobagem
@ JesperJuhl - certo, mas estou perguntando por que não pode , não por que pode , e a regra "como se" geralmente se aplica ao primeiro, mas não faz sentido para o último. Além disso, "como se" não está claro para o layout da estrutura, que geralmente é uma preocupação global, não local. Por fim, o compilador precisa ter um único conjunto consistente de regras para o layout, exceto, talvez, para estruturas que possam provar que nunca "escapam".
BeeOnRope 22/01
11
@BeeOnRope Não posso responder à sua pergunta, desculpe. É por isso que acabei de publicar um comentário e não uma resposta. O que você obteve nesse comentário foi o meu melhor palpite para uma explicação, mas eu não sei a resposta (interessante para aprender eu mesmo - e é por isso que você recebeu um voto positivo).
Jesper Juhl 22/01
11
@ NicolBolas - você está respondendo à pergunta certa? Não se trata de detectar cópias seguras ou qualquer outra coisa. Antes, estou curioso para saber por que o preenchimento não pode ser reutilizado entre os membros. De qualquer forma, você está errado: trivialmente copiável é uma propriedade do tipo e sempre foi. No entanto, para copiar de forma segura um objeto que ele deve ambos têm um tipo TC (uma propriedade do tipo), e não ser um sujeito-potencialmente-sobreposição (a propriedade do objeto, que eu acho que é onde você ficou confuso). Ainda não sabemos por que estamos falando de cópias aqui.
BeeOnRope 22/01

Respostas:

1

O padrão é extremamente silencioso ao falar sobre o modelo de memória e não muito explícito sobre alguns dos termos que ele usa. Mas acho que encontrei uma argumentação funcional (que pode ser um pouco fraca)

Primeiro, vamos descobrir o que faz parte de um objeto. [tipos básicos] / 4 :

A representação do objeto de um objeto do tipo Té a sequência de N unsigned charobjetos ocupados pelo objeto do tipo T, onde Né igual a sizeof(T). A representação do valor de um objeto do tipo Té o conjunto de bits que participam na representação de um valor do tipo T. Bits na representação do objeto que não fazem parte da representação do valor são bits de preenchimento.

Portanto, a representação do objeto b0consiste em sizeof(blub) unsigned charobjetos, portanto, 8 bytes. Os bits de preenchimento fazem parte do objeto.

Nenhum objeto pode ocupar o espaço de outro se não estiver aninhado dentro dele [basic.life] /1.5 :

A vida útil de um objeto odo tipo Ttermina quando:

[...]

(1.5) o armazenamento que o objeto ocupa é liberado ou é reutilizado por um objeto que não está aninhado dentro o([intro.object]).

Assim, a vida útil b0terminaria, quando o armazenamento ocupado por ela fosse reutilizado por outro objeto, ou seja b1. Eu não verifiquei isso, mas acho que o padrão exige que o subobjeto de um objeto vivo também esteja vivo (e eu não podia imaginar como isso deveria funcionar de maneira diferente).

Portanto, o armazenamento que b0 ocupa não pode ser usado por b1. Não encontrei nenhuma definição de "ocupar" no padrão, mas acho que uma interpretação razoável seria "parte da representação do objeto". Na representação do objeto que descreve a cotação, as palavras "ocupar" são usadas 1 . Aqui, isso seria 8 bytes, então blaprecisa de pelo menos mais um b1.

Especialmente para subobjetos (portanto, entre outros membros de dados não estáticos), também existe a estipulação [intro.object] / 9 (mas foi adicionada com C ++ 20, thx @BeeOnRope)

Dois objetos com vida útil sobreposta que não são campos de bits podem ter o mesmo endereço se um estiver aninhado no outro ou se pelo menos um for um subobjeto de tamanho zero e forem de tipos diferentes; caso contrário, eles têm endereços distintos e ocupam bytes de armazenamento separados .

(ênfase minha) Aqui, novamente, temos o problema que "ocupa" não está definido e, novamente, eu argumentaria que assumimos os bytes na representação do objeto. Observe que há uma nota de rodapé a este [basic.memobj] / nota de rodapé 29

Sob a regra "como se", uma implementação pode armazenar dois objetos no mesmo endereço da máquina ou não armazenar um objeto, se o programa não puder observar a diferença ([introdução.execução]).

O que pode permitir ao compilador quebrar isso se puder provar que não há efeito colateral observável. Eu acho que isso é bastante complicado para algo tão fundamental como o layout do objeto. Talvez seja por isso que essa otimização é feita apenas quando o usuário fornece as informações de que não há razão para ter objetos não-adicionais adicionando o [no_unique_address]atributo

tl; dr: Preenchimento talvez parte do objeto e membros tenham que ser disjuntos.


1 Não pude resistir a acrescentar uma referência que ocupe pode significar retomar: Dicionário Revisitado e Sem Compromisso de Webster, G. & C. Merriam, 1913 (grifo meu)

  1. Manter ou preencher as dimensões de; ocupar a sala ou espaço de; cobrir ou encher; como, o acampamento ocupa cinco acres de terra. Sir J. Herschel.

Qual rastreamento padrão estaria completo sem um rastreamento de dicionário?

n314159
fonte
2
A parte "ocupar bytes de armazenamento disjuntos" de into.storage seria suficiente, eu acho, para mim - mas essa redação foi adicionada apenas no C ++ 20 como parte da alteração adicionada no_unique_address. Isso deixa a situação anterior ao C ++ 20 menos clara. Não entendi o seu raciocínio que leva a "Nenhum objeto pode ocupar o espaço de outro se não estiver aninhado dentro dele" em basic.life/1.5, em particular como obter "do armazenamento que o objeto ocupa é liberado" para "nenhum objeto pode ocupar o espaço de outro".
BeeOnRope 22/01
11
Eu adicionei um pequeno esclarecimento a esse parágrafo. Espero que isso torne mais compreensível. Caso contrário, voltarei a ver amanhã, agora é muito tarde para mim.
n314159 22/01
"Dois objetos com vida útil sobreposta que não são campos de bits podem ter o mesmo endereço se um estiver aninhado no outro ou se pelo menos um for um subobjeto de tamanho zero e forem de tipos diferentes" 2 objetos com vida útil sobreposta, de o mesmo tipo, tem o mesmo endereço .
Language Lawyer
Desculpe, você poderia elaborar? Você está citando uma citação padrão da minha resposta e trazendo um exemplo que conflita um pouco com isso. Não tenho certeza se este é um comentário sobre minha resposta e se é o que deveria me dizer. Em relação ao seu exemplo, eu diria que é preciso considerar ainda outras partes do padrão (há um parágrafo sobre uma matriz de caracteres não assinada que fornece armazenamento para outro objeto, algo referente à otimização da base de tamanho zero e ainda mais, deve-se procurar também se o posicionamento novo subsídios especiais, todas as coisas que eu não acho relevantes para o exemplo dos OPs)
n314159 23/01
@ n314159 Acho que esta formulação pode estar com defeito.
Language Lawyer