yoast paginas pagina indexar google evitar duplicado contenido configurar como categorias aplicación c++ c struct

c++ - google - indexar paginas



¿Es legal indexar en una estructura? (10)

Aquí hay una manera de usar una clase proxy para acceder a los elementos en una matriz de miembros por nombre. Es muy C ++, y no tiene ningún beneficio frente a las funciones de acceso de retorno, excepto por la preferencia sintáctica. Esto sobrecarga al operador -> para acceder a los elementos como miembros, por lo que para ser aceptable, uno debe rechazar la sintaxis de los accesores ( da() = 5; ), así como tolerar el uso de -> con un objeto sin puntero. Supongo que esto también podría confundir a los lectores que no están familiarizados con el código, por lo que esto podría ser más un buen truco que algo que desea poner en producción.

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

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

La construcción de un objeto Proxy puebla sus 3 miembros de referencia a , c acuerdo 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 clase Data , esto ahorra memoria al llenar 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 siguiente código incluye 2 versiones de main() . La versión #if 1 usa los operadores -> y [] , y la versión #if 0 realiza el conjunto de procedimientos equivalente, pero solo accediendo directamente a Data::ar .

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

Para gcc 6.2, usando -O3, ambas versiones de main() generan el mismo ensamblaje (alternar entre #if 1 y #if 0 antes del primer 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

Independientemente de cuán ''malo'' sea el código, y suponiendo que la alineación, etc., no sea un problema en el compilador / plataforma, ¿es este comportamiento indefinido o roto?

Si tengo una estructura como esta: -

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

¿Es legal acceder a , c como (&thing.a)[0] , (&thing.a)[1] y (&thing.a)[2] ?

En todos los casos, en cada compilador y plataforma lo probé, con cada configuración que probé ''funcionó''. Solo me preocupa que el compilador no se dé cuenta de que by cosa [1] son lo mismo y que las tiendas a ''b'' podrían colocarse en un registro y que cosa [1] lee el valor incorrecto de la memoria (por ejemplo). Sin embargo, en todos los casos lo intenté, hizo lo correcto. (Me doy cuenta, por supuesto, que no prueba mucho)

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

Etiquetado C y C ++. Estoy principalmente interesado en C ++ pero también en C si es diferente, solo por interés.


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 se garantiza que funcione, sino que el uso es más simple, no necesita escribir expresiones ilegibles (&thing.a)[0]

Nota: esta respuesta se da en el supuesto de que ya tiene una estructura con campos y necesita agregar acceso a través del í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]; } };


En C ++, este es un comportamiento mayormente indefinido (depende de qué índice).

De [expr.unary.op]:

Para fines de aritmética de puntero (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 lo tanto, la expresión &thing.a se refiere a una matriz de un 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 agrega o resta de un puntero, el resultado tiene el tipo del operando del puntero. Si la expresión P apunta al elemento x[i] de un objeto de matriz x con n elementos, las expresiones P + J y J + P (donde J tiene el valor j ) apuntan al elemento (posiblemente hipotético) x[i + j] si 0 <= i + j <= n ; de lo contrario, el comportamiento es indefinido.

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

(&thing.a)[2] viola la condición previa de que 0 <= i + j <= n , ya que tenemos i == 0 , j == 2 , n == 1 . Simplemente construyendo el puntero &thing.a + 2 es un comportamiento indefinido.

(&thing.a)[1] es el caso interesante. En realidad no viola nada en [expr.add]. Se nos permite tomar 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) ocupada 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 pasado el final de un objeto (5.7) apunta 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 + 1 es un comportamiento definido, pero desreferenciarlo no está definido porque no apunta a nada.


En ISO C99 / C11, el punteo de tipo basado en la unión es legal, por lo que puede usarlo en lugar de indexar punteros a no matrices (consulte varias otras respuestas).

ISO C ++ no permite el punteo de tipos basado en la unión. GNU C ++ lo hace, como una extensión , y creo que algunos otros compiladores que no son compatibles con las extensiones de GNU en general sí admiten el tipo de punción de unión. Pero eso no te ayuda a escribir código estrictamente portátil.

Con las versiones actuales de gcc y clang, escribir una función de miembro de C ++ usando un switch(idx) para seleccionar un miembro se optimizará para índices constantes en tiempo de compilación, pero producirá un asm terrible ramificado para índices de tiempo de ejecución. No hay nada intrínsecamente malo con switch() para 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 a esto es hacerlo de la otra manera: proporcione 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 asm para diferentes casos de uso, en el explorador del compilador Godbolt . Estas son funciones completas del Sistema V x86-64, con la instrucción RET final omitida para mostrar mejor lo que obtendría cuando estuvieran 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 un switch() para C ++ hace un asm como este para un índice variable en 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 castigo tipo basada en la unión 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]


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

Estás tomando a 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 un conjunto contiguo no vacío 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 citas anteriores deberían ser suficientes para indicar por qué indexar en una struct como lo hizo no es un comportamiento definido por el estándar C ++, (&thing.a)[2] un ejemplo: observe la expresión (&thing.a)[2] - Con respecto al subíndice operador:

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

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

[expr.add/4] : cuando una expresión que tiene un tipo integral se agrega o resta de un puntero, el resultado tiene el tipo del operando del puntero. Si la expresión P apunta al elemento x[i] de un objeto de matriz x con n elementos, las expresiones P + J y J + P (donde J tiene el valor j ) apuntan al elemento (posiblemente hipotético) x[i + j] si 0 ≤ i + j ≤ n ; de lo contrario , el comportamiento es indefinido. ...

Tenga en cuenta el requisito de matriz para la cláusula if ; de 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 ampliamente el código y sus variaciones en varios compiladores y no introducen ningún relleno aquí ( funciona ); desde una vista de 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 sigue siendo un comportamiento indefinido ...

Algunas soluciones han proporcionado algunas soluciones viables (con 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 a la thing.a miembro de la estructura a través de esta parte.


Es ilegal, pero hay una solución alternativa:

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

Ahora puedes indexar v:


Este es un comportamiento indefinido.

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

Hay reglas sobre alias (acceso a datos a través de dos tipos de puntero 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 suponer que ningún acceso a la matriz basada en [] puede modificarla. Por lo tanto, no tiene que recargar constantemente los datos de la memoria cada vez que los usa; solo si alguien podría haberlo modificado por su nombre .

Por lo tanto (&thing.a)[1] puede ser asumido por el compilador para no referirse a thing.b . Puede usar este hecho para reordenar lecturas y escrituras en thing.b , invalidando lo que quiere 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 obtienes un compilador que dice 7 y luego 2! = 7, y luego dos punteros idénticos; a pesar de que ptr apunta a x . El compilador toma el hecho de que x es un valor constante para no molestarse en leerlo cuando solicita el valor de x .

Pero cuando tomas la dirección de x , la obligas a existir. Luego desechas const y lo modificas. Entonces, la ubicación real en la memoria donde se ha modificado x , ¡el compilador es libre de no leerla realmente cuando lee x !

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

Puede proporcionar un operator[] personalizado operator[] que obtenga el elemento correcto.

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

Tener ambos es útil.


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) e intenta usarlo como una matriz 2 para acceder al siguiente elemento hipotético, provoca un comportamiento indefinido, incluso si sucede que hay otro objeto del mismo tipo en esa dirección

Sin embargo, puede usar la dirección del objeto struct 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 se debe hacer para cada miembro individualmente, pero se puede poner en una función que se asemeja a un acceso de 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 el operando de un operador unario * que se evalúa.

2 (Citado de: ISO / IEC 9899: 201x 6.5.6 Operadores aditivos 7)
Para los fines 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 de longitud uno con el tipo de objeto como su tipo de elemento.


Para c ++: si necesita acceder a un miembro sin conocer su nombre, puede usar una variable puntero a 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;


Si leer valores es suficiente, y la eficiencia no es una preocupación, o si confía en su compilador para optimizar las cosas bien, o si struct es solo esos 3 bytes, puede hacer esto con seguridad:

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 ++, es probable que desee utilizar static_assert para verificar que los struct data tengan un diseño estándar, y tal vez lanzar una excepción en un índice no válido.