É legal indexar em uma estrutura?

104

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, be ccomo (&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.

jcoder
fonte
51
Não, não é "legal". É um comportamento indefinido.
Sam Varshavchik
10
Funciona para você neste caso muito simples porque o compilador não adiciona nenhum preenchimento entre os membros. Experimente estruturas que usam tipos de tamanhos diferentes e desabarão.
Algum programador de
7
Desenterrando o passado - UB costumava ser apelidado de daemons nasais .
Adrian Colomitchi
21
Muito bem, aqui eu tropeço porque sigo a tag C, leio a pergunta e escrevo uma resposta que só se aplica a C, porque não vi a tag C ++. C e C ++ são muito diferentes aqui! C permite trocadilhos com uniões, C ++ não.
Lundin
7
Se você precisar acessar os elementos como um array, defina-os como um array. Se eles precisarem de nomes diferentes, use os nomes. Tentar ter seu bolo e comê-lo acabará levando à indigestão - provavelmente no momento mais inconveniente que se possa imaginar. (Acho que o índice 0 é legal em C; o índice 1 ou 2 não. Existem contextos nos quais um único elemento é tratado como uma matriz de tamanho 1.)
Jonathan Leffler

Respostas:

73

É 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):

[dcl.array / 1] : ... Um objeto do tipo array contém umconjunto não vazio alocado de forma contígua de N subobjetos do tipo T ...

Mas, para membros, não existe tal requisito contíguo :

[class.mem / 17] : ...; Os requisitos de alinhamento de implementação podem fazer com que dois membros adjacentes não sejam alocados imediatamente um após o outro ...

Embora as duas aspas acima devam ser suficientes para sugerir por que indexar em um structcomo 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:

[expr.post//expr.sub/1] : Uma expressão pós-fixada seguida por uma expressão entre colchetes é uma expressão pós-fixada. Uma das expressões deve ser um glvalue do tipo “array de T” ou um prvalue do tipo “ponteiro para T” e a outra deve ser um prvalue de enumeração sem escopo ou tipo integral. O resultado é do tipo “T”. O tipo “T” deve ser um tipo de objeto completamente definido.66 A expressão E1[E2]é idêntica (por definição) a((E1)+(E2))

Investigando o texto em negrito da citação acima: sobre como adicionar um tipo integral a um tipo de ponteiro (observe a ênfase aqui) ..

[expr.add / 4] : Quando uma expressão que possui tipo integral é adicionada ou subtraída de um ponteiro, o resultado tem o tipo do operando ponteiro. Se a expressãoPaponta para o elementox[i]de um objeto de xmatriz com n elementos, as expressõesP + JeJ + P(ondeJtem o valorj) apontam para o elemento (possivelmente hipotético)x[i + j] se0 ≤ i + j ≤ n; caso contrário , o comportamento é indefinido. ...

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.amembro da estrutura por meio deste parttern.

WhiZTiM
fonte
1
@jcoder É definido em class.mem . Veja o último parágrafo para o texto real.
NathanOliver
4
Alisar estritamente não é relevante aqui. O tipo int está contido no tipo agregado e esse tipo pode ser apelidado de int. - 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),
2501 de
1
@Os downvoters, gostaria de comentar? - e para melhorar ou apontar onde esta resposta está errada?
WhiZTiM
4
O aliasing estrito é irrelevante para isso. O preenchimento não faz parte do valor armazenado de um objeto. Além disso, esta resposta não aborda o caso mais comum: o que acontece quando não há preenchimento. Recomendaria excluir esta resposta, na verdade.
MM de
1
Feito! Removi o parágrafo sobre aliasing estrito.
WhiZTiM
48

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:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

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.

2501
fonte
3
Observe que isso só funciona se a classe for um tipo de layout padrão. Se não, ainda é UB.
NathanOliver
@NathanOliver Devo mencionar que minha resposta se aplica apenas a C. Editado. Este é um dos problemas de tais questões de linguagem de tag dupla.
2501 de
Obrigado, e é por isso que solicitei C ++ e C separadamente, pois é interessante saber as diferenças
jcoder
@NathanOliver O endereço do primeiro membro é garantido para coincidir com o endereço da classe C ++ se for o layout padrão. No entanto, isso não garante que o acesso seja bem definido nem implica que tais acessos em outras classes sejam indefinidos.
Potatoswatter
você diria que isso char* p = ( char* )&thing.a + offsetof( thing , b );leva a um comportamento indefinido?
MM de
43

Em C ++, se você realmente precisar - crie o operador []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

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:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Esta solução mudaria o tamanho da estrutura para que você também pudesse usar métodos:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};
Slava
fonte
1
Eu adoraria ver a desmontagem disso, em vez da desmontagem de um programa C usando trocadilhos. Mas, mas ... C ++ é tão rápido quanto C ... certo? Certo?
Lundin
6
@Lundin se você se preocupa com a velocidade dessa construção, os dados devem ser organizados como um array em primeiro lugar, não como campos separados.
Slava
2
@Lundin em ambos, você quer dizer comportamento ilegível e indefinido? Não, obrigado.
Slava
1
A sobrecarga do operador @Lundin é um recurso sintático em tempo de compilação que não induz nenhuma sobrecarga em comparação com as funções normais. Dê uma olhada em godbolt.org/g/vqhREz para ver o que o compilador realmente faz quando compila o código C ++ e C. É incrível o que eles fazem e o que se espera que façam. Eu pessoalmente prefiro melhor segurança de tipo e expressividade de C ++ em vez de C um milhão de vezes. E funciona o tempo todo sem depender de suposições sobre preenchimento.
Jens
2
Essas referências vão dobrar o tamanho da coisa, pelo menos. Apenas faça thing.a().
TC
14

Para c ++: se você precisar acessar um membro sem saber seu nome, poderá usar um ponteiro para a variável de membro.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;
StoryTeller - Unslander Monica
fonte
1
Isso é usar as facilidades da linguagem e, como resultado, é bem definido e, como suponho, eficiente. Melhor resposta.
Peter - Reintegrar Monica de
2
Suponha que seja eficiente? Eu presumo o oposto. Observe o código gerado.
JDługosz de
1
@ JDługosz, você está certo. Dando uma olhada na montagem gerada, parece que o gcc 6.2 cria um código equivalente ao uso offsetoffem C.
StoryTeller - Unslander Monica
3
você também pode melhorar as coisas tornando arr constexpr. Isso criará uma única tabela de pesquisa fixa na seção de dados em vez de criá-la instantaneamente.
Tim
10

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 com switch()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.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

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.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

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

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

Isso é obviamente terrível, em comparação com a versão de trocadilhos baseada em união C (ou GNU C ++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]
Peter Cordes
fonte
@MM: bom ponto. É mais uma resposta a vários comentários e uma alternativa à resposta de Slava. Reformulei a parte inicial, de modo que pelo menos comece como uma resposta à pergunta original. Obrigado por apontar isso.
Peter Cordes
Embora o tipo de trocadilho baseado em união pareça funcionar no gcc e clang ao usar o []operador diretamente em um membro do sindicato, o padrão define array[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 a someUnion. A única explicação que posso ver é que someUnion.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.
supercat
9

Em C ++, isso é principalmente um comportamento indefinido (depende de qual índice).

De [expr.unary.op]:

Para fins de aritmética de ponteiro (5.7) e comparação (5.9, 5.10), um objeto que não é um elemento de array cujo endereço é obtido dessa forma é considerado como pertencente a um array com um elemento do tipo T.

A expressão &thing.aé, portanto, considerada como se referindo a uma matriz de umint .

De [expr.sub]:

A expressão E1[E2]é idêntica (por definição) a*((E1)+(E2))

E de [expr.add]:

Quando uma expressão com tipo integral é adicionada ou subtraída de um ponteiro, o resultado tem o tipo do operando ponteiro. Se a expressão Paponta para o elemento x[i]de um objeto de matriz xcom nelementos, as expressões P + Je J + P(onde Jtem o valor j) apontam para o elemento (possivelmente hipotético) x[i + j]se 0 <= i + j <= n; caso contrário, o comportamento é indefinido.

(&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 que 0 <= i + j <= n, uma vez que temos i == 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]:

Um valor de um tipo de ponteiro que é um ponteiro para ou após o final de um objeto representa o endereço do primeiro byte na memória (1.7) ocupado pelo objeto 53 ou o primeiro byte na memória após o final do armazenamento ocupado pelo objeto , respectivamente. [Observação: um ponteiro além do final de um objeto (5.7) não é considerado como apontando para um objeto não relacionado do tipo do objeto que pode estar localizado naquele endereço.

Portanto, pegar o ponteiro &thing.a + 1é um comportamento definido, mas desreferenciá-lo é indefinido porque ele não aponta para nada.

Barry
fonte
Avaliar (& thing.a) + 1 é quase legal porque um ponteiro além do final de um array é legal; ler ou gravar os dados armazenados ali é um comportamento indefinido, comparando com & thing.b com <,>, <=,> = é um comportamento indefinido. (& thing.a) + 2 é absolutamente ilegal.
gnasher729 de
@ gnasher729 Sim, vale a pena esclarecer a resposta um pouco mais.
Barry
Este (&thing.a + 1)é um caso interessante que não consegui cobrir. +1! ... Só por curiosidade, você está no comitê ISO C ++?
WhiZTiM
É também um caso muito importante porque, caso contrário, todo loop usando ponteiros como um intervalo semiaberto seria UB.
Jens
Em relação à última citação padrão. C ++ é mais bem especificado do que C aqui.
2501 de
8

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 a thing.b. Ele pode usar esse fato para reordenar leituras e gravações thing.b, invalidando o que você deseja, sem invalidar o que você realmente disse para fazer.

Um exemplo clássico disso é descartar const.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

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 ptrestá apontando x. O compilador considera o fato de que xé um valor constante para não se incomodar em lê-lo quando você pergunta o valor de x.

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 onde xestá foi modificado, o compilador está livre para não lê-lo realmente durante a leitura x!

O compilador pode ficar esperto o suficiente para descobrir como evitar até mesmo seguir ptrpara ler *ptr, mas muitas vezes não é. Sinta-se à vontade para usar ptr = ptr+argc-1ou 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.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

ter ambos é útil.

Yakk - Adam Nevraumont
fonte
"o fato de não ser membro de um array significa que o compilador pode assumir que nenhum acesso ao array baseado em [] pode modificá-lo." - não é verdade, por exemplo, (&thing.a)[0]pode modificá-lo
MM
Não vejo como o exemplo const tem algo a ver com a questão. Isso só falha porque há uma regra específica de que um objeto const não pode ser modificado, por nenhum outro motivo.
MM de
1
@MM, não é um exemplo de indexação em uma estrutura, mas é uma ilustração muito boa de como usar o comportamento indefinido para fazer referência a algo por sua localização aparente na memória pode resultar em uma saída diferente do esperado, porque o compilador pode fazer outra coisa com o UB do que você queria.
Wildcard
@MM Desculpe, nenhum acesso de array diferente de um trivial através de um ponteiro para o próprio objeto. E o segundo é apenas um exemplo de efeitos colaterais fáceis de ver do comportamento indefinido; o compilador otimiza as leituras xporque sabe que você não pode alterá-lo de uma maneira definida. Otimização semelhante pode ocorrer quando você altera bvia (&blah.a)[1]se o compilador puder provar que não houve acesso definido para balterá-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.
Yakk - Adam Nevraumont
6

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 Dataestrutura neste código também inclui sobrecargas para o operador subscrito, para acessar elementos indexados dentro de seu armembro de matriz, bem como begineend 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 Datas ->é usado para acessar um elemento por nome (como este my_data->b = 5;:), um Proxyobjeto é retornado. Então, como esse Proxyrvalue não é um ponteiro, seu próprio ->operador é chamado de cadeia automática, que retorna um ponteiro para si mesmo. Dessa forma, o Proxyobjeto é instanciado e permanece válido durante a avaliação da expressão inicial.

A construção de um Proxyobjeto preenche seus 3 membros de referência a, be de cacordo 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 modelo T. Portanto, em vez de usar referências nomeadas que são membros da Dataclasse, 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 de main(). A #if 1versão usa os operadores ->e [], e a #if 0versã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 1e #if 0antes do primeiro main()para comparar): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif
Christopher Oicles
fonte
Nifty. Votado principalmente porque você provou que isso otimiza totalmente. BTW, você pode fazer isso muito mais facilmente escrevendo uma função muito simples, não um todo main()com funções de temporização! por exemplo, int getb(Data *d) { return (*d)->b; }compila apenas para mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Sim, Data &dtornaria a sintaxe mais fácil, mas usei um ponteiro em vez de ref para destacar a estranheza de sobrecarregar ->dessa forma.)
Peter Cordes
Enfim, isso é legal. Outras ideias como int tmp[] = { a, b, c}; return tmp[idx];não otimizam totalmente, então é legal que esta o faça.
Peter Cordes
Mais um motivo de minha falta operator.em C ++ 17.
Jens de
2

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:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Para a versão somente C ++, você provavelmente gostaria de usar static_assertpara verificar se struct datatem layout padrão e, talvez, lançar uma exceção no índice inválido.

hyde
fonte
1

É ilegal, mas há uma solução alternativa:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Agora você pode indexar v:

Sven Nilsson
fonte
6
Muitos projetos c ++ acham que fazer downcast em todos os lugares está bem. Ainda não devemos pregar práticas ruins.
StoryTeller - Unslander Monica
2
A união resolve o problema de aliasing estrito em ambos os idiomas. Mas fazer trocadilhos por meio de sindicatos só funciona em C, não em C ++.
Lundin
1
ainda assim, eu não ficaria surpreso se isso funcionasse em 100% de todos os compiladores c ++. sempre.
Sven Nilsson
1
Você pode tentar no gcc com as configurações de otimizador mais agressivas ativadas.
Lundin
1
@Lundin: o trocadilho do tipo união é legal no GNU C ++, como uma extensão do ISO C ++. Não parece estar declarado muito claramente no manual , mas tenho quase certeza disso. Ainda assim, essa resposta precisa explicar onde é válida e onde não é.
Peter Cordes