Independentemente de quão 'ruim' o código seja, e assumindo que o alinhamento etc. não seja um problema no compilador / plataforma, esse comportamento é indefinido ou está quebrado?
Se eu tiver uma estrutura como esta: -
struct data
{
int a, b, c;
};
struct data thing;
É legal para o acesso a
, b
e c
como (&thing.a)[0]
, (&thing.a)[1]
e(&thing.a)[2]
?
Em todos os casos, em todos os compiladores e plataformas em que tentei, com todas as configurações que experimentei 'funcionou'. Eu só estou preocupado que o compilador pode não perceber que b e coisa [1] são a mesma coisa e lojas para 'b' pode ser colocado em um registo e coisa [1] lê o valor errado de memória (por exemplo). Em todos os casos, eu tentei, mas deu certo. (Eu percebo que isso não prova muito)
Este não é meu código; é o código com o qual tenho que trabalhar, estou interessado em saber se este é um código ruim ou quebrado , pois a diferença afeta minhas prioridades para mudá-lo muito :)
Marcado como C e C ++. Estou mais interessado em C ++, mas também em C, se for diferente, apenas por interesse.
Respostas:
É ilegal 1 . Esse é um comportamento indefinido em C ++.
Você está pegando os membros em uma forma de array, mas aqui está o que o padrão C ++ diz (ênfase minha):
Mas, para membros, não existe tal requisito contíguo :
Embora as duas aspas acima devam ser suficientes para sugerir por que indexar em um
struct
como você fez não é um comportamento definido pelo padrão C ++, vamos escolher um exemplo: observe a expressão(&thing.a)[2]
- Em relação ao operador subscrito:Investigando o texto em negrito da citação acima: sobre como adicionar um tipo integral a um tipo de ponteiro (observe a ênfase aqui) ..
Observe o requisito de array para a cláusula if ; senão o contrário na citação acima. A expressão
(&thing.a)[2]
obviamente não se qualifica para a cláusula if ; Conseqüentemente, comportamento indefinido.Em uma nota lateral: embora eu tenha experimentado extensivamente o código e suas variações em vários compiladores e eles não introduzam nenhum preenchimento aqui ( funciona ); de uma visão de manutenção, o código é extremamente frágil. você ainda deve afirmar que a implementação alocou os membros de forma contígua antes de fazer isso. E fique dentro dos limites :-). Mas ainda é um comportamento indefinido ....
Algumas soluções alternativas viáveis (com comportamento definido) foram fornecidas por outras respostas.
Como corretamente apontado nos comentários, [basic.lval / 8] , que estava na minha edição anterior, não se aplica. Obrigado @ 2501 e @MM
1 : Veja a resposta de @Barry a esta pergunta para o único caso legal em que você pode acessar um
thing.a
membro da estrutura por meio deste parttern.fonte
- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
Não. Em C, esse é um comportamento indefinido, mesmo que não haja preenchimento.
O que causa o comportamento indefinido é o acesso fora dos limites 1 . Quando você tem um escalar (membros a, b, c na estrutura) e tenta usá-lo como uma matriz 2 para acessar o próximo elemento hipotético, você causa um comportamento indefinido, mesmo se houver outro objeto do mesmo tipo em esse endereço.
No entanto, você pode usar o endereço do objeto de estrutura e calcular o deslocamento em um membro específico:
Isso deve ser feito para cada membro individualmente, mas pode ser colocado em uma função que se assemelha a um acesso de array.
1 (Citado de: ISO / IEC 9899: 201x 6.5.6 Operadores aditivos 8)
Se o resultado apontar um após o último elemento do objeto de matriz, ele não deve ser usado como o operando de um operador unário * que é avaliado.
2 (Citado de: ISO / IEC 9899: 201x 6.5.6 Operadores aditivos 7)
Para os fins desses operadores, um ponteiro para um objeto que não é um elemento de uma matriz se comporta da mesma forma que um ponteiro para o primeiro elemento de um array de comprimento um com o tipo do objeto como seu tipo de elemento.
fonte
char* p = ( char* )&thing.a + offsetof( thing , b );
leva a um comportamento indefinido?Em C ++, se você realmente precisar - crie o operador []:
não é apenas garantido que funcione, mas o uso é mais simples, você não precisa escrever uma expressão ilegível
(&thing.a)[0]
Nota: esta resposta é dada no pressuposto de que você já tem uma estrutura com campos e precisa adicionar acesso via índice. Se a velocidade for um problema e você puder alterar a estrutura, isso pode ser mais eficaz:
Esta solução mudaria o tamanho da estrutura para que você também pudesse usar métodos:
fonte
thing.a()
.Para c ++: se você precisar acessar um membro sem saber seu nome, poderá usar um ponteiro para a variável de membro.
fonte
offsetoff
em C.No ISO C99 / C11, o tipo de trocadilho baseado em união é legal, então você pode usar isso em vez de indexar ponteiros para não matrizes (veja várias outras respostas).
ISO C ++ não permite trocadilhos baseados em união. GNU C ++ faz, como uma extensão , e eu acho que alguns outros compiladores que não suportam extensões GNU em geral suportam o tipo de punção de união. Mas isso não o ajuda a escrever código estritamente portátil.
Com as versões atuais do gcc e clang, escrever uma função de membro C ++ usando um
switch(idx)
para selecionar um membro otimizará os índices constantes de tempo de compilação, mas produzirá um conjunto terrível de ramificações para índices de tempo de execução. Não há nada inerentemente errado comswitch()
isso; este é simplesmente um bug de otimização perdida nos compiladores atuais. Eles poderiam compilar a função switch () do Slava de forma eficiente.A solução / solução alternativa para isso é fazer da outra maneira: fornecer à sua classe / estrutura um membro de matriz e escrever funções de acesso para anexar nomes a elementos específicos.
Podemos dar uma olhada na saída do ASM para diferentes casos de uso, no explorador do compilador Godbolt . Essas são funções completas do x86-64 System V, com a instrução RET final omitida para mostrar melhor o que você obteria quando fossem incorporadas. ARM / MIPS / qualquer coisa seria semelhante.
Por comparação, a resposta de @Slava usando um
switch()
para C ++ torna-se assim para um índice de variável de tempo de execução. (Código no link Godbolt anterior).Isso é obviamente terrível, em comparação com a versão de trocadilhos baseada em união C (ou GNU C ++):
fonte
[]
operador diretamente em um membro do sindicato, o padrão definearray[index]
como sendo equivalente a*((array)+(index))
, e nem o gcc nem o clang reconhecerão com segurança que um acesso a*((someUnion.array)+(index))
é um acesso asomeUnion
. A única explicação que posso ver é quesomeUnion.array[index]
nem*((someUnion.array)+(index))
não são definidos pelo padrão, mas são meramente uma extensão popular, e gcc / clang optou por não oferecer suporte ao segundo, mas parece oferecer suporte ao primeiro, pelo menos por agora.Em C ++, isso é principalmente um comportamento indefinido (depende de qual índice).
De [expr.unary.op]:
A expressão
&thing.a
é, portanto, considerada como se referindo a uma matriz de umint
.De [expr.sub]:
E de [expr.add]:
(&thing.a)[0]
está perfeitamente bem formado porque&thing.a
é considerado uma matriz de tamanho 1 e estamos obtendo esse primeiro índice. Esse é um índice permitido.(&thing.a)[2]
viola a pré-condição de que0 <= i + j <= n
, uma vez que temosi == 0
,j == 2
,n == 1
. A simples construção do ponteiro&thing.a + 2
é um comportamento indefinido.(&thing.a)[1]
é o caso interessante. Na verdade, não viola nada em [expr.add]. Temos permissão para fazer um ponteiro após o final da matriz - o que seria. Aqui, nos voltamos para uma observação em [basic.compound]:Portanto, pegar o ponteiro
&thing.a + 1
é um comportamento definido, mas desreferenciá-lo é indefinido porque ele não aponta para nada.fonte
(&thing.a + 1)
é um caso interessante que não consegui cobrir. +1! ... Só por curiosidade, você está no comitê ISO C ++?Este é um comportamento indefinido.
Existem muitas regras em C ++ que tentam dar ao compilador alguma esperança de entender o que você está fazendo, para que ele possa raciocinar sobre isso e otimizá-lo.
Existem regras sobre aliasing (acesso a dados por meio de dois tipos diferentes de ponteiros), limites de matriz, etc.
Quando você tem uma variável
x
, o fato de ela não ser membro de um array significa que o compilador pode assumir que nenhum[]
acesso baseado no array pode modificá-lo. Portanto, não é necessário recarregar constantemente os dados da memória sempre que você os usa; somente se alguém pudesse modificá-lo de seu nome .Portanto
(&thing.a)[1]
, o compilador pode presumir que ele não se refere athing.b
. Ele pode usar esse fato para reordenar leituras e gravaçõesthing.b
, invalidando o que você deseja, sem invalidar o que você realmente disse para fazer.Um exemplo clássico disso é descartar const.
aqui você normalmente obtém um compilador dizendo 7 então 2! = 7, e então dois ponteiros idênticos; apesar do fato de que
ptr
está apontandox
. O compilador considera o fato de quex
é um valor constante para não se incomodar em lê-lo quando você pergunta o valor dex
.Mas quando você pega o endereço de
x
, você o força a existir. Você então descarta const e o modifica. Portanto, o local real na memória ondex
está foi modificado, o compilador está livre para não lê-lo realmente durante a leiturax
!O compilador pode ficar esperto o suficiente para descobrir como evitar até mesmo seguir
ptr
para ler*ptr
, mas muitas vezes não é. Sinta-se à vontade para usarptr = ptr+argc-1
ou fazer alguma confusão se o otimizador estiver ficando mais inteligente do que você.Você pode fornecer um personalizado
operator[]
que obtenha o item certo.ter ambos é útil.
fonte
(&thing.a)[0]
pode modificá-lox
porque sabe que você não pode alterá-lo de uma maneira definida. Otimização semelhante pode ocorrer quando você alterab
via(&blah.a)[1]
se o compilador puder provar que não houve acesso definido parab
alterá-lo; tal mudança pode ocorrer devido a mudanças aparentemente inócuas no compilador, código circundante ou qualquer outro. Portanto, nem mesmo testar se funciona é suficiente.Esta é uma maneira de usar uma classe proxy para acessar elementos em uma matriz de membro por nome. É muito C ++ e não tem nenhum benefício em relação às funções de acesso de retorno de referência, exceto para preferência sintática. Isso sobrecarrega o
->
operador para acessar elementos como membros, portanto, para ser aceitável, é necessário não gostar da sintaxe de acessadores (d.a() = 5;
), bem como tolerar o uso->
com um objeto que não seja um ponteiro. Espero que isso também confunda os leitores não familiarizados com o código, então isso pode ser mais um truque interessante do que algo que você deseja colocar em produção.A
Data
estrutura neste código também inclui sobrecargas para o operador subscrito, para acessar elementos indexados dentro de seuar
membro de matriz, bem comobegin
eend
funções , para iteração. Além disso, todos eles estão sobrecarregados com versões não constantes e const, que eu senti que precisavam ser incluídas para completar.Quando
Data
s->
é usado para acessar um elemento por nome (como estemy_data->b = 5;
:), umProxy
objeto é retornado. Então, como esseProxy
rvalue não é um ponteiro, seu próprio->
operador é chamado de cadeia automática, que retorna um ponteiro para si mesmo. Dessa forma, oProxy
objeto é instanciado e permanece válido durante a avaliação da expressão inicial.A construção de um
Proxy
objeto preenche seus 3 membros de referênciaa
,b
e dec
acordo com um ponteiro passado no construtor, que se supõe apontar para um buffer contendo pelo menos 3 valores cujo tipo é dado como o parâmetro do modeloT
. Portanto, em vez de usar referências nomeadas que são membros daData
classe, isso economiza memória ao preencher as referências no ponto de acesso (mas, infelizmente, usando->
e não o.
operador).Para testar o quão bem o otimizador do compilador elimina todos os caminhos indiretos introduzidos pelo uso de
Proxy
, o código a seguir inclui 2 versões demain()
. A#if 1
versão usa os operadores->
e[]
, e a#if 0
versão executa o conjunto equivalente de procedimentos, mas apenas acessando diretamenteData::ar
.A
Nci()
função gera valores inteiros de tempo de execução para inicializar elementos da matriz, o que impede o otimizador de apenas inserir valores constantes diretamente em cadastd::cout
<<
chamada.Para gcc 6.2, usando -O3, ambas as versões de
main()
geram o mesmo assembly (alterne entre#if 1
e#if 0
antes do primeiromain()
para comparar): https://godbolt.org/g/QqRWZbfonte
main()
com funções de temporização! por exemplo,int getb(Data *d) { return (*d)->b; }
compila apenas paramov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Sim,Data &d
tornaria a sintaxe mais fácil, mas usei um ponteiro em vez de ref para destacar a estranheza de sobrecarregar->
dessa forma.)int tmp[] = { a, b, c}; return tmp[idx];
não otimizam totalmente, então é legal que esta o faça.operator.
em C ++ 17.Se ler valores for suficiente e a eficiência não for uma preocupação, ou se você confia em seu compilador para otimizar as coisas bem, ou se struct tiver apenas 3 bytes, você pode fazer isso com segurança:
Para a versão somente C ++, você provavelmente gostaria de usar
static_assert
para verificar sestruct data
tem layout padrão e, talvez, lançar uma exceção no índice inválido.fonte
É ilegal, mas há uma solução alternativa:
Agora você pode indexar v:
fonte