¿Es legal indexar en una estructura?

104

Independientemente de lo "malo" que sea el código, y suponiendo que la alineación, etc., no sea un problema en el compilador / plataforma, ¿este comportamiento no está definido o no funciona?

Si tengo una estructura como esta: -

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

struct data thing;

¿Es legal para el acceso a, by ccomo (&thing.a)[0], (&thing.a)[1]y (&thing.a)[2]?

En todos los casos, en cada compilador y plataforma en los que lo probé, con cada configuración que probé, 'funcionó'. Sólo estoy preocupado de que el compilador podría no darse cuenta de que b y cosa [1] son la misma cosa y almacena a 'b' podrían ser puestos en un registro y lo [1] lee el valor incorrecto de la memoria (por ejemplo). Sin embargo, en todos los casos que lo intenté, hice lo correcto. (Me doy cuenta, por supuesto, que eso no prueba mucho)

Este no es mi código; es el código con el que tengo que trabajar, estoy interesado en si se trata de un código incorrecto o un código roto , ya que los diferentes afectan mis prioridades para cambiarlo mucho :)

Etiquetado C y C ++. Lo que más me interesa es C ++, pero también C si es diferente, solo por interés.

jcoder
fuente
51
No, no es legal". Es un comportamiento indefinido.
Sam Varshavchik
10
Funciona para usted en este caso muy simple porque el compilador no agrega ningún relleno entre los miembros. Pruebe con estructuras que usen tipos de diferentes tamaños y se derrumbarán.
Un tipo programador
7
Desenterrando el pasado - UB solía ser apodado demonios nasales .
Adrian Colomitchi
21
Bueno, genial, aquí me tropiezo porque sigo la etiqueta C, leo la pregunta y luego escribo una respuesta que solo se aplica a C, porque no vi la etiqueta C ++. ¡C y C ++ son muy diferentes aquí! C permite juegos de palabras con uniones, C ++ no.
Lundin
7
Si necesita acceder a los elementos como una matriz, defínalos como una matriz. Si necesitan tener nombres diferentes, use los nombres. Tratar de tener su pastel y comérselo eventualmente conducirá a la indigestión, probablemente en el momento más inconveniente imaginable. (Creo que el índice 0 es legal en C; el índice 1 o 2 no lo es. Hay contextos en los que un solo elemento se trata como una matriz de tamaño 1.)
Jonathan Leffler

Respuestas:

73

Es ilegal 1 . Ese es un comportamiento indefinido en C ++.

Está tomando los miembros en forma de matriz, pero esto es lo que dice el estándar C ++ (énfasis mío):

[dcl.array / 1] : ... Un objeto de tipo matriz contiene unconjunto no vacío asignado contiguamente de N subobjetos de tipo T ...

Pero, para los miembros, no existe un requisito contiguo :

[class.mem / 17] : ...; Los requisitos de alineación de implementación pueden hacer que dos miembros adyacentes no se asignen inmediatamente uno después del otro ...

Si bien las dos comillas anteriores deberían ser suficientes para indicar por qué la indexación en a structcomo lo hizo no es un comportamiento definido por el estándar C ++, escojamos un ejemplo: observe la expresión (&thing.a)[2]- Con respecto al operador de subíndice:

[expr.post//expr.sub/1] : una expresión de sufijo seguida de una expresión entre corchetes es una expresión de sufijo. Una de las expresiones será un valor gl de tipo “matriz de T” o un valor pr de tipo “puntero a T” y la otra será un valor pr de enumeración sin ámbito o tipo integral. El resultado es de tipo “T”. El tipo "T" será un tipo de objeto completamente definido.66 La expresión E1[E2]es idéntica (por definición) a((E1)+(E2))

Profundizando en el texto en negrita de la cita anterior: con respecto a agregar un tipo integral a un tipo de puntero (tenga en cuenta el énfasis aquí).

[expr.add / 4] : cuando una expresión que tiene un tipo integral se suma o se resta de un puntero, el resultado tiene el tipo de operando del puntero. Si la expresiónPapunta a un elementox[i]de un objeto de xmatriz con n elementos, las expresionesP + JyJ + P(dondeJtiene el valorj) apuntan al elemento (posiblemente hipotético)x[i + j] si0 ≤ i + j ≤ n; de lo contrario , el comportamiento no está definido. ...

Tenga en cuenta el requisito de matriz para la cláusula if ; de lo contrario, lo contrario en la cita anterior. La expresión (&thing.a)[2]obviamente no califica para la cláusula if ; Por lo tanto, comportamiento indefinido.


En una nota al margen: aunque he experimentado extensamente el código y sus variaciones en varios compiladores y no introducen ningún relleno aquí, ( funciona ); desde el punto de vista del mantenimiento, el código es extremadamente frágil. aún debe afirmar que la implementación asignó los miembros de forma contigua antes de hacer esto. Y mantente dentro de los límites :-). Pero su comportamiento aún indefinido ...

Otras respuestas han proporcionado algunas soluciones viables (con un comportamiento definido).



Como se señaló correctamente en los comentarios, [basic.lval / 8] , que estaba en mi edición anterior, no se aplica. Gracias @ 2501 y @MM

1 : Vea la respuesta de @ Barry a esta pregunta para el único caso legal en el que puede acceder al thing.amiembro de la estructura a través de este parttern.

WhiZTiM
fuente
1
@jcoder Está definido en class.mem . Consulte el último párrafo para ver el texto real.
NathanOliver
4
La estricta clasificación no es relevante aquí. El tipo int está contenido dentro del tipo agregado y este tipo puede alias 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
1
@Los votantes negativos, ¿les gustaría comentar? - ¿Y para mejorar o señalar dónde está incorrecta esta respuesta?
WhiZTiM
4
El alias estricto es irrelevante para esto. El relleno no forma parte del valor almacenado de un objeto. Además, esta respuesta no aborda el caso más común: qué sucede cuando no hay relleno. Recomendaría eliminar esta respuesta en realidad.
MM
1
¡Hecho! He eliminado el párrafo sobre el alias estricto.
WhiZTiM
48

No. En C, este es un comportamiento indefinido incluso si no hay relleno.

Lo que causa un comportamiento indefinido es el acceso fuera de los límites 1 . Cuando tiene un escalar (miembros a, b, c en la estructura) y trata de usarlo como una matriz 2 para acceder al siguiente elemento hipotético, causa un comportamiento indefinido, incluso si hay otro objeto del mismo tipo en esa dirección.

Sin embargo, puede usar la dirección del objeto de estructura y calcular el desplazamiento en un miembro específico:

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

Esto debe hacerse para cada miembro individualmente, pero se puede poner en una función que se parezca a un acceso a una matriz.


1 (Citado de: ISO / IEC 9899: 201x 6.5.6 Operadores aditivos 8)
Si el resultado apunta uno más allá del último elemento del objeto de matriz, no se utilizará como operando de un operador unario * que se evalúe.

2 (Citado de: ISO / IEC 9899: 201x 6.5.6 Operadores aditivos 7)
Para los propósitos de estos operadores, un puntero a un objeto que no es un elemento de una matriz se comporta igual que un puntero al primer elemento de una matriz. matriz de longitud uno con el tipo de objeto como su tipo de elemento.

2501
fuente
3
Tenga en cuenta que esto solo funciona si la clase es un tipo de diseño estándar. Si no, sigue siendo UB.
NathanOliver
@NathanOliver Debo mencionar que mi respuesta solo se aplica a C. Editado. Este es uno de los problemas de estas preguntas de lenguaje dual.
2501
Gracias, y es por eso que pedí por separado C ++ y C, ya que es interesante conocer las diferencias
jcoder
@NathanOliver Se garantiza que la dirección del primer miembro coincidirá con la dirección de la clase C ++ si es de diseño estándar. Sin embargo, eso no garantiza que el acceso esté bien definido ni implica que dichos accesos en otras clases no estén definidos.
Potatoswatter
¿Diría que char* p = ( char* )&thing.a + offsetof( thing , b );conduce a un comportamiento indefinido?
MM
43

En C ++, si realmente lo necesita, cree el 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

no solo está garantizado que funcione, sino que su uso es más simple, no es necesario escribir una expresión ilegible (&thing.a)[0]

Nota: esta respuesta se da asumiendo que ya tiene una estructura con campos y necesita agregar acceso a través de índice. Si la velocidad es un problema y puede cambiar la estructura, esto podría ser más efectivo:

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

Esta solución cambiaría el tamaño de la estructura para que también pueda usar métodos:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};
Slava
fuente
1
Me encantaría ver el desmontaje de esto, en comparación con el desmontaje de un programa en C usando juegos de palabras. Pero, pero ... C ++ es tan rápido como C ... ¿verdad? ¿Correcto?
Lundin
6
@Lundin, si le importa la velocidad de esta construcción, los datos deben organizarse como una matriz en primer lugar, no como campos separados.
Slava
2
@Lundin en ambos, ¿te refieres a un comportamiento ilegible e indefinido? No, gracias.
Slava
1
La sobrecarga del operador @Lundin es una característica sintáctica en tiempo de compilación que no induce ninguna sobrecarga en comparación con las funciones normales. Eche un vistazo a godbolt.org/g/vqhREz para ver qué hace realmente el compilador cuando compila el código C ++ y C. Es asombroso lo que hacen y lo que uno espera que hagan. Personalmente, prefiero una mejor seguridad de tipos y expresividad de C ++ sobre C un millón de veces. Y funciona todo el tiempo sin depender de suposiciones sobre el relleno.
Jens
2
Esas referencias duplicarán el tamaño de la cosa al menos. Solo hazlo thing.a().
TC
14

Para c ++: si necesita acceder a un miembro sin saber su nombre, puede usar un puntero a la variable miembro.

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
fuente
1
Esto está utilizando las facilidades del lenguaje, y como resultado está bien definido y, como supongo, eficiente. La mejor respuesta.
Peter - Reincorpora a Monica
2
¿Asume eficiente? Asumo lo contrario. Mira el código generado.
JDługosz
1
@ JDługosz, tienes toda la razón. Echando un vistazo al ensamblado generado, parece que gcc 6.2 crea un código equivalente al uso offsetoffen C.
StoryTeller - Unslander Monica
3
también puede mejorar las cosas haciendo arr constexpr. Esto creará una única tabla de búsqueda fija en la sección de datos en lugar de crearla sobre la marcha.
Tim
10

En ISO C99 / C11, el tipo de juego de palabras basado en uniones es legal, por lo que puede usarlo en lugar de indexar punteros a no matrices (consulte varias otras respuestas).

ISO C ++ no permite juegos de palabras basados ​​en uniones. GNU C ++ lo hace, como una extensión , y creo que algunos otros compiladores que no son compatibles con las extensiones GNU en general sí admiten el juego de palabras de tipo union. Pero eso no le ayuda a escribir código estrictamente portátil.

Con las versiones actuales de gcc y clang, escribir una función miembro de C ++ usando a switch(idx)para seleccionar un miembro se optimizará para índices constantes en tiempo de compilación, pero producirá un asm terrible y ramificado para índices en tiempo de ejecución. No hay nada intrínsecamente malo switch()en esto; esto es simplemente un error de optimización perdido en los compiladores actuales. Podrían compilar la función switch () de Slava de manera eficiente.


La solución / solución alternativa a esto es hacerlo de la otra manera: dé a su clase / estructura un miembro de matriz y escriba funciones de acceso para adjuntar nombres 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 echar un vistazo a la salida de asm para diferentes casos de uso, en el explorador del compilador Godbolt . Estas son funciones completas de System V x86-64, con la instrucción RET final omitida para mostrar mejor lo que obtendría cuando estén en línea. ARM / MIPS / lo que sea similar.

# 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]

En comparación, la respuesta de @ Slava usando a switch()para C ++ hace que asm sea así para un índice de variable de tiempo de ejecución. (Código en el enlace 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

Esto es obviamente terrible, en comparación con la versión de juego de palabras basada en la unión de C (o 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
fuente
@MM: buen punto. Es más una respuesta a varios comentarios y una alternativa a la respuesta de Slava. Reformulé la parte inicial, por lo que al menos comienza como una respuesta a la pregunta original. Gracias por señalar eso.
Peter Cordes
Si bien el juego de palabras basado en sindicatos parece funcionar en gcc y clang mientras se usa el []operador directamente en un miembro del sindicato, el Estándar define array[index]como equivalente a *((array)+(index)), y ni gcc ni clang reconocerán de manera confiable que un acceso a *((someUnion.array)+(index))es un acceso a someUnion. La única explicación que puedo ver es que someUnion.array[index]no *((someUnion.array)+(index))están definidos por el Estándar, sino que son simplemente extensiones populares, y gcc / clang han optado por no admitir el segundo, pero parecen admitir el primero, al menos por ahora.
supercat
9

En C ++, este es principalmente un comportamiento indefinido (depende del índice).

De [expr.unary.op]:

Para propósitos de aritmética de punteros (5.7) y comparación (5.9, 5.10), un objeto que no es un elemento de matriz cuya dirección se toma de esta manera se considera que pertenece a una matriz con un elemento de tipo T.

Por &thing.atanto, se considera que la expresión se refiere a una matriz de uno int.

De [expr.sub]:

La expresión E1[E2]es idéntica (por definición) a*((E1)+(E2))

Y de [expr.add]:

Cuando una expresión que tiene un tipo integral se suma o se resta de un puntero, el resultado tiene el tipo del operando del puntero. Si la expresión Papunta a un elemento x[i]de un objeto de matriz xcon nelementos, las expresiones P + Jy J + P(donde Jtiene el valor j) apuntan al elemento (posiblemente hipotético) x[i + j]si 0 <= i + j <= n; de lo contrario, el comportamiento no está definido.

(&thing.a)[0]está perfectamente bien formado porque &thing.ase considera una matriz de tamaño 1 y estamos tomando ese primer índice. Ese es un índice permitido.

(&thing.a)[2]viola la condición previa de que 0 <= i + j <= n, puesto que tenemos i == 0, j == 2, n == 1. La simple construcción del puntero &thing.a + 2es un comportamiento indefinido.

(&thing.a)[1]es el caso interesante. En realidad, no viola nada en [expr.add]. Se nos permite llevar un puntero más allá del final de la matriz, que sería este. Aquí, pasamos a una nota en [basic.compound]:

Un valor de un tipo de puntero que es un puntero hacia o más allá del final de un objeto representa la dirección del primer byte en la memoria (1.7) ocupado por el objeto53 o el primer byte en la memoria después del final del almacenamiento ocupado por el objeto , respectivamente. [Nota: No se considera que un puntero más allá del final de un objeto (5.7) apunte a un objeto no relacionado del tipo de objeto que podría estar ubicado en esa dirección.

Por lo tanto, tomar el puntero &thing.a + 1es un comportamiento definido, pero desreferenciarlo no está definido porque no apunta a nada.

Barry
fuente
Evaluar (& thing.a) + 1 es casi legal porque un puntero más allá del final de una matriz es legal; leer o escribir los datos almacenados allí es un comportamiento indefinido, comparar con & thing.b con <,>, <=,> = es un comportamiento indefinido. (& thing.a) + 2 es absolutamente ilegal.
gnasher729
@ gnasher729 Sí, vale la pena aclarar la respuesta un poco más.
Barry
El (&thing.a + 1)es un caso interesante, no pude cubrir. +1! ... Solo curiosidad, ¿estás en el comité de ISO C ++?
WhiZTiM
También es un caso muy importante porque, de lo contrario, cada bucle que use punteros como un intervalo medio abierto sería UB.
Jens
Respecto a la última cita estándar. C ++ debe especificarse mejor que C aquí.
2501
8

Este es un comportamiento indefinido.

Hay muchas reglas en C ++ que intentan darle al compilador alguna esperanza de entender lo que estás haciendo, para que pueda razonar y optimizarlo.

Hay reglas sobre el alias (acceso a datos a través de dos tipos de punteros diferentes), límites de matriz, etc.

Cuando tiene una variable x, el hecho de que no sea miembro de una matriz significa que el compilador puede asumir que ningún []acceso a la matriz basada puede modificarla. Por lo tanto, no tiene que recargar constantemente los datos de la memoria cada vez que la usa; sólo si alguien pudiera haberlo modificado de su nombre .

Por (&thing.a)[1]lo tanto, el compilador puede asumir que no se hace referencia a thing.b. Puede usar este hecho para reordenar las lecturas y las escrituras thing.b, invalidando lo que desea que haga sin invalidar lo que realmente le dijo que hiciera.

Un ejemplo clásico de esto es desechar 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';

aquí normalmente aparece un compilador que dice 7, luego 2! = 7, y luego dos punteros idénticos; a pesar de que ptrestá apuntando x. El compilador toma el hecho de que xes un valor constante para no molestarse en leerlo cuando le pregunta el valor de x.

Pero cuando tomas la dirección de x, la obligas a existir. Luego desecha la constante y la modifica. Entonces, la ubicación real en la memoria donde xse ha modificado, ¡el compilador es libre de no leerlo al leerlo x!

El compilador puede volverse lo suficientemente inteligente como para descubrir cómo evitar incluso seguir ptrpara leer *ptr, pero a menudo no lo es. Siéntase libre de usar ptr = ptr+argc-1o alguna confusión si el optimizador se está volviendo más inteligente que usted.

Puede proporcionar una costumbre operator[]que obtenga el artículo correcto.

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

tener ambos es útil.

Yakk - Adam Nevraumont
fuente
"el hecho de que no sea miembro de un arreglo significa que el compilador puede asumir que ningún acceso al arreglo basado en [] puede modificarlo". - no es cierto, por ejemplo, (&thing.a)[0]puede modificarlo
MM
No veo cómo el ejemplo const tiene algo que ver con la pregunta. Eso falla solo porque hay una regla específica de que un objeto constante no puede modificarse, ni por ninguna otra razón.
MM
1
@MM, no es un ejemplo de indexación en una estructura, pero es una muy buena ilustración de cómo el uso de un comportamiento indefinido para hacer referencia a algo por su ubicación aparente en la memoria, puede resultar en una salida diferente a la esperada, porque el compilador puede hacer otra cosa con la UB de lo que querías.
Comodín
@MM Lo sentimos, no hay acceso a la matriz que no sea trivial a través de un puntero al objeto en sí. Y el segundo es solo un ejemplo de los efectos secundarios fáciles de ver de un comportamiento indefinido; el compilador optimiza las lecturas xporque sabe que no puede cambiarlo de una manera definida. Una optimización similar podría ocurrir cuando modificas bvia (&blah.a)[1]si el compilador puede probar que no había un acceso definido bque pudiera alterarlo; tal cambio podría ocurrir debido a cambios aparentemente inocuos en el compilador, el código circundante o lo que sea. Así que incluso probar que funciona no es suficiente.
Yakk - Adam Nevraumont
6

Esta es una forma de utilizar una clase de proxy para acceder a los elementos de una matriz de miembros por su nombre. Es muy C ++ y no tiene ningún beneficio en comparación con las funciones de acceso que devuelven ref, excepto por la preferencia sintáctica. Esto sobrecarga al ->operador para acceder a elementos como miembros, por lo que para ser aceptable, es necesario que no le guste la sintaxis de los accesores ( d.a() = 5;), así como tolerar el uso ->con un objeto que no sea puntero. Espero que esto también confunda a los lectores que no estén familiarizados con el código, por lo que podría ser más un truco ingenioso que algo que quieras poner en producción.

La Dataestructura en este código también incluye sobrecargas para el operador de subíndice, para acceder a elementos indexados dentro de su armiembro de matriz, así como funciones beginy end, para iteración. Además, todos estos están sobrecargados con versiones no const y const, que sentí que debían incluirse para completar.

Cuando se usa Data's ->para acceder a un elemento por su nombre (como este :), se devuelve my_data->b = 5;un Proxyobjeto. Entonces, debido a que este Proxyrvalue no es un puntero, su propio ->operador se llama en cadena automática, que devuelve un puntero a sí mismo. De esta manera, Proxyse crea una instancia del objeto y permanece válido durante la evaluación de la expresión inicial.

La construcción de un Proxyobjeto llena sus 3 miembros de referencia a, by de cacuerdo con un puntero pasado en el constructor, que se supone que apunta a un búfer que contiene al menos 3 valores cuyo tipo se da como parámetro de plantilla T. Entonces, en lugar de usar referencias con nombre que son miembros de la Dataclase, esto ahorra memoria al completar las referencias en el punto de acceso (pero desafortunadamente, usando ->y no el .operador).

Para probar qué tan bien el optimizador del compilador elimina toda la indirección introducida por el uso de Proxy, el código siguiente incluye 2 versiones de main(). La #if 1versión usa los operadores ->y [], y la #if 0versión realiza el conjunto equivalente de procedimientos, pero solo accediendo directamente Data::ar.

La Nci()función genera valores enteros en tiempo de ejecución para inicializar elementos de la matriz, lo que evita que el optimizador simplemente conecte valores constantes directamente en cada std::cout <<llamada.

Para gcc 6.2, usando -O3, ambas versiones de main()generan el mismo ensamblado (alternar entre #if 1y #if 0antes del primero 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
fuente
Hábil. Votó a favor principalmente porque demostró que esto optimiza. Por cierto, puede hacerlo mucho más fácilmente escribiendo una función muy simple, ¡no un todo main()con funciones de temporización! por ejemplo, se int getb(Data *d) { return (*d)->b; }compila solo en mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Sí, Data &dfacilitaría la sintaxis, pero utilicé un puntero en lugar de una referencia para resaltar la rareza de la sobrecarga de ->esta manera.)
Peter Cordes
De todos modos, esto es genial. Otras ideas como int tmp[] = { a, b, c}; return tmp[idx];no optimizar, así que está bien que esta lo haga.
Peter Cordes
Una razón más que extraño operator.en C ++ 17.
Jens
2

Si leer valores es suficiente y la eficiencia no es un problema, o si confía en que su compilador optimizará bien las cosas, o si la estructura es solo de 3 bytes, puede hacer esto de manera segura:

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 la versión solo de C ++, probablemente desee usar static_assertpara verificar que struct datatiene un diseño estándar y tal vez lanzar una excepción en un índice no válido.

Hyde
fuente
1

Es ilegal, pero hay una solución:

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

Ahora puede indexar v:

Sven Nilsson
fuente
6
Muchos proyectos de C ++ piensan que el abatimiento por todas partes está bien. Todavía no deberíamos predicar malas prácticas.
StoryTeller - Unslander Monica
2
El sindicato resuelve el problema estricto de los alias en ambos idiomas. Pero el juego de palabras a través de uniones solo está bien en C, no en C ++.
Lundin
1
aún así, no me sorprendería que esto funcione en el 100% de todos los compiladores de C ++. nunca.
Sven Nilsson
1
Puede probarlo en gcc con la configuración de optimizador más agresiva activada.
Lundin
1
@Lundin: el juego de palabras de tipo union es legal en GNU C ++, como una extensión sobre ISO C ++. No parece estar muy claro en el manual , pero estoy bastante seguro de esto. Aún así, esta respuesta debe explicar dónde es válida y dónde no lo es.
Peter Cordes