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
, b
y c
como (&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.
Respuestas:
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):
Pero, para los miembros, no existe un requisito contiguo :
Si bien las dos comillas anteriores deberían ser suficientes para indicar por qué la indexación en a
struct
como 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: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í).
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.a
miembro de la estructura a través de este parttern.fuente
- 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),
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:
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.
fuente
char* p = ( char* )&thing.a + offsetof( thing , b );
conduce a un comportamiento indefinido?En C ++, si realmente lo necesita, cree el operador []:
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:
Esta solución cambiaría el tamaño de la estructura para que también pueda usar métodos:
fuente
thing.a()
.Para c ++: si necesita acceder a un miembro sin saber su nombre, puede usar un puntero a la variable miembro.
fuente
offsetoff
en C.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 maloswitch()
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.
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.
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).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 ++):
fuente
[]
operador directamente en un miembro del sindicato, el Estándar definearray[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 asomeUnion
. La única explicación que puedo ver es quesomeUnion.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.En C ++, este es principalmente un comportamiento indefinido (depende del índice).
De [expr.unary.op]:
Por
&thing.a
tanto, se considera que la expresión se refiere a una matriz de unoint
.De [expr.sub]:
Y de [expr.add]:
(&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.(&thing.a)[2]
viola la condición previa de que0 <= i + j <= n
, puesto que tenemosi == 0
,j == 2
,n == 1
. La simple construcción del 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 llevar un puntero más allá del final de la matriz, que sería este. Aquí, pasamos a una nota en [basic.compound]:Por lo tanto, tomar el puntero
&thing.a + 1
es un comportamiento definido, pero desreferenciarlo no está definido porque no apunta a nada.fuente
(&thing.a + 1)
es un caso interesante, no pude cubrir. +1! ... Solo curiosidad, ¿estás en el comité de ISO C ++?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 athing.b
. Puede usar este hecho para reordenar las lecturas y las escriturasthing.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.
aquí normalmente aparece un compilador que dice 7, luego 2! = 7, y luego dos punteros idénticos; a pesar de que
ptr
está apuntandox
. El compilador toma el hecho de quex
es un valor constante para no molestarse en leerlo cuando le pregunta el valor dex
.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 dondex
se ha modificado, ¡el compilador es libre de no leerlo al leerlox
!El compilador puede volverse lo suficientemente inteligente como para descubrir cómo evitar incluso seguir
ptr
para leer*ptr
, pero a menudo no lo es. Siéntase libre de usarptr = ptr+argc-1
o 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.tener ambos es útil.
fuente
(&thing.a)[0]
puede modificarlox
porque sabe que no puede cambiarlo de una manera definida. Una optimización similar podría ocurrir cuando modificasb
via(&blah.a)[1]
si el compilador puede probar que no había un acceso definidob
que 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.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
Data
estructura en este código también incluye sobrecargas para el operador de subíndice, para acceder a elementos indexados dentro de suar
miembro de matriz, así como funcionesbegin
yend
, 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 devuelvemy_data->b = 5;
unProxy
objeto. Entonces, debido a que esteProxy
rvalue no es un puntero, su propio->
operador se llama en cadena automática, que devuelve un puntero a sí mismo. De esta manera,Proxy
se crea una instancia del objeto y permanece válido durante la evaluación de la expresión inicial.La construcción de un
Proxy
objeto llena sus 3 miembros de referenciaa
,b
y dec
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 plantillaT
. Entonces, en lugar de usar referencias con nombre que son miembros de laData
clase, 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 demain()
. La#if 1
versión usa los operadores->
y[]
, y la#if 0
versión realiza el conjunto equivalente de procedimientos, pero solo accediendo directamenteData::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 cadastd::cout
<<
llamada.Para gcc 6.2, usando -O3, ambas versiones de
main()
generan el mismo ensamblado (alternar entre#if 1
y#if 0
antes del primeromain()
para comparar): https://godbolt.org/g/QqRWZbfuente
main()
con funciones de temporización! por ejemplo, seint getb(Data *d) { return (*d)->b; }
compila solo enmov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Sí,Data &d
facilitaría la sintaxis, pero utilicé un puntero en lugar de una referencia para resaltar la rareza de la sobrecarga de->
esta manera.)int tmp[] = { a, b, c}; return tmp[idx];
no optimizar, así que está bien que esta lo haga.operator.
en C ++ 17.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:
Para la versión solo de C ++, probablemente desee usar
static_assert
para verificar questruct data
tiene un diseño estándar y tal vez lanzar una excepción en un índice no válido.fuente
Es ilegal, pero hay una solución:
Ahora puede indexar v:
fuente