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 int
4 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 blub
e blob
, mesmo que o tamanho 1 blob
possa, em princípio, "encaixar" no preenchimento de blub
.
O C ++ 20 apresenta o no_unique_address
atributo, 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 bla
cai 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::memcpy
de 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 blub
e blob
não trivialmente copiáveis .
fonte
Respostas:
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 :
Portanto, a representação do objeto
b0
consiste emsizeof(blub)
unsigned char
objetos, 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 :
Assim, a vida útil
b0
terminaria, quando o armazenamento ocupado por ela fosse reutilizado por outro objeto, ou sejab1
. 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 porb1
. 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ãobla
precisa de pelo menos mais umb1
.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)
(ê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
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]
atributotl; 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)
Qual rastreamento padrão estaria completo sem um rastreamento de dicionário?
fonte
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".