c++ c gcc standards

c++ - En el operador de coma, ¿se garantiza que el operando izquierdo no se ejecute realmente si no tiene efectos secundarios?



gcc standards (6)

Para mostrar el tema, voy a usar C, pero la misma macro se puede usar también en C ++ (con o sin struct ), lo que genera la misma pregunta.

Se me ocurrió esta macro

#define STR_MEMBER(S,X) (((struct S*)NULL)->X, #X)

Su propósito es tener cadenas ( const char* ) de un miembro existente de una struct , de modo que si el miembro no existe, la compilación falla. Un ejemplo de uso mínimo:

#include <stdio.h> struct a { int value; }; int main(void) { printf("a.%s member really exists/n", STR_MEMBER(a, value)); return 0; }

Si el value no fuera un miembro de la struct a , el código no se compilaría, y esto es lo que quería.

El operador de coma debe evaluar el operando de la izquierda y luego descartar el resultado de la expresión (si hay uno), de modo que entiendo que este operador generalmente se usa cuando la evaluación del operando de la izquierda tiene efectos secundarios.

En este caso, sin embargo, no hay efectos secundarios (previstos), pero, por supuesto, funciona si el compilador no produce realmente el código que evalúa la expresión, ya que de lo contrario accedería a una struct ubicada en NULL y una segmentación la culpa se produciría

Gcc / g ++ 6.3 y 4.9.2 nunca produjeron ese código peligroso, incluso con -O0 , como si siempre pudieran "ver" que la evaluación no tiene efectos secundarios y, por lo tanto, se puede omitir.

Agregar volatile en la macro (por ejemplo, porque acceder a esa dirección de memoria es el efecto secundario deseado) fue hasta ahora la única forma de desencadenar el fallo de segmentación.

Entonces, la pregunta: ¿hay algo en los estándares de C y C ++ que garantice que los compiladores siempre eviten la evaluación real del operando izquierdo del operador de coma cuando el compilador puede estar seguro de que la evaluación no tiene efectos secundarios?

Notas y arreglos

No estoy pidiendo un juicio sobre la macro como es y la oportunidad de usarla o mejorarla. Para el propósito de esta pregunta, la macro es mala si y solo si evoca un comportamiento indefinido, es decir, si y solo si es arriesgado porque los compiladores pueden generar el "código de evaluación" incluso cuando esto no tiene efectos secundarios.

Ya tengo dos soluciones obvias en mente: "reificar" la struct y usar offsetof . El primero necesita un área de memoria accesible tan grande como la struct más grande que usamos como primer argumento de STR_MEMBER (por ejemplo, tal vez una unión estática podría hacer ...). Este último debería funcionar perfectamente: proporciona un desplazamiento en el que no estamos interesados ​​y evita el problema de acceso; de hecho, asumo gcc , porque es el compilador que uso (de ahí la etiqueta) y que su offsetof integrado se comporta.

Con el offsetof arreglo, la macro se convierte

#define STR_MEMBER(S,X) (offsetof(struct S,X), #X)

Escribir volatile struct S lugar de struct S no causa la segfault.

Sugerencias sobre otras posibles "correcciones" también son bienvenidas.

Nota añadida

En realidad, el caso de uso real estaba en C ++ en una struct almacenamiento estática. Esto parece estar bien en C ++, pero tan pronto como probé C con un código más cercano al original en lugar del hervido para esta pregunta, me di cuenta de que C no está nada contento con eso:

error: initializer element is not constant

C quiere que la estructura sea inicializable en el momento de la compilación, en lugar de eso C ++ está bien con eso.


¿Hay algo en el estándar de lenguajes C y C ++ que garantice que los compiladores siempre evitarán la evaluación real del operando izquierdo del operador de coma?

Es lo contrario. El estándar garantiza que el operando de la izquierda se evalúa (en realidad lo hace, no hay excepciones). El resultado es descartado.

Nota: para expresiones lvalue, "evaluar" no significa "acceder al valor almacenado". En su lugar, significa averiguar dónde está la ubicación de memoria designada. El otro código que abarca la expresión lvalue puede o no continuar para acceder a la ubicación de la memoria. El proceso de lectura de la ubicación de la memoria se conoce como "conversión de lvalor" en C, o "conversión de lvalor a rvalor" en C ++.

En C ++, una expresión de valor descartado (como el operando izquierdo del operador de coma) solo tiene una conversión de valor a rvalue realizada si es volatile y también cumple con otros criterios (consulte C ++ 14 [expr] / 11 para más detalles) ). En C lvalue, la conversión ocurre para expresiones cuyo resultado no se usa (C11 6.3.2.1/2).

En su ejemplo, es discutible si ocurre o no la conversión de valores. En ambos idiomas, X->Y , donde X es un puntero, se define como (*X).Y ; en C, el acto de aplicar * a un puntero nulo ya causa un comportamiento indefinido (C11 6.5.3 / 3), y en C ++ el . el operador solo se define para el caso en el que el operando izquierdo designa realmente un objeto (C ++ 14 [expr.ref] /4.2).


Gcc / g ++ 6.3 y 4.9.2 nunca produjeron ese código peligroso, incluso con -O0, como si siempre pudieran "ver" que la evaluación no tiene efectos secundarios y, por lo tanto, se puede omitir.

clang producirá un código que genera un error si lo pasas con la opción -fsanitize=undefined . Lo que debería responder a su pregunta: al menos uno de los desarrolladores principales de la implementación considera claramente que el código tiene un comportamiento indefinido. Y son correctos.

Sugerencias sobre otras posibles "correcciones" también son bienvenidas.

Buscaría algo que esté garantizado para no evaluar la expresión. Su sugerencia de offsetof hace el trabajo, pero en ocasiones puede hacer que se rechace un código que, de lo contrario, se aceptaría, como cuando X es ab . Si quieres que eso sea aceptado, mi pensamiento sería usar sizeof para forzar que una expresión permanezca sin evaluar.


El operador de coma (la documentación C , dice algo muy similar) no tiene tales garantías.

En una expresión de coma E1, E2 , la expresión E1 se evalúa, su resultado se descarta ... y sus efectos secundarios se completan antes de que comience la evaluación de la expresión E2

información irrelevante omitida

En pocas palabras, se evaluará E1 , aunque el compilador podría optimizarlo mediante la regla as-if si es capaz de determinar que no hay efectos secundarios.


El lenguaje no necesita decir nada acerca de la "ejecución real" debido a la regla de si-si . Después de todo, sin efectos secundarios, ¿cómo podría saber si la expresión se evalúa? (Mirar el ensamblaje o establecer puntos de interrupción no cuenta; eso no es parte de la ejecución del programa, que es todo lo que describe el lenguaje).

Por otro lado, eliminar la referencia a un puntero nulo es un comportamiento indefinido, por lo que el lenguaje no dice nada sobre lo que sucede. No puede esperar como y si para salvarlo: como si es una relajación de las restricciones plausibles de otra manera en la implementación, y el comportamiento indefinido es una relajación de todas las restricciones en la implementación. Por lo tanto, no hay "conflicto" entre "esto no tiene efectos secundarios, por lo que podemos ignorarlo" y "este es un comportamiento indefinido, así que los demonios nasales"; están del mismo lado!


El operando izquierdo del operador de coma es una expresión de valor descartado

5 expresiones
11 En algunos contextos, una expresión solo aparece por sus efectos secundarios. Dicha expresión se denomina expresión de valor descartado. La expresión es evaluada y su valor es descartado. [...]

También hay operandos no evaluados que, como su nombre lo indica, no se evalúan.

8 En algunos contextos, aparecen operandos no evaluados (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). Un operando no evaluado no se evalúa. Un operando no evaluado se considera una expresión completa. [...]

Usar una expresión de valor descartado en su caso de uso es un comportamiento indefinido, pero usar un operando no evaluado no lo es.

Usar sizeof por ejemplo, no causaría UB porque toma un operando no evaluado.

#define STR_MEMBER(S,X) (sizeof(S::X), #X)

sizeof es preferible a offsetof , porque offsetof no se puede usar para miembros estáticos y clases que no son de diseño estándar:

18 biblioteca de soporte de idiomas
4 La macro offsetof (tipo, miembro-designador) acepta un conjunto restringido de argumentos de tipo en esta Norma Internacional. Si el tipo no es una clase de diseño estándar (Cláusula 9), los resultados no están definidos. [...] El resultado de aplicar offsetof macro a un campo que es un miembro de datos estáticos o un miembro de función no está definido. [...]


Usted pregunta,

¿Hay algo en el estándar de lenguajes C y C ++ que garantice que los compiladores siempre evitarán la evaluación real del operando izquierdo del operador de coma cuando el compilador puede estar seguro de que la evaluación no tiene efectos secundarios?

Como han dicho otros, la respuesta es "no". Por el contrario, los estándares establecen incondicionalmente que el operando de la izquierda del operador de coma se evalúa y que el resultado se descarta.

Esto es, por supuesto, una descripción del modelo de ejecución de una máquina abstracta; Se permite que las implementaciones funcionen de manera diferente, siempre que el comportamiento observable sea el mismo que produciría el comportamiento de la máquina abstracta. Si de hecho la evaluación de la expresión de la mano izquierda no produce efectos secundarios, entonces eso permitiría omitirla por completo, pero no hay nada en ninguno de los estándares que prevea que sea necesario omitirla.

En cuanto a solucionarlo, tiene varias opciones, algunas de las cuales se aplican solo a uno u otro de los dos idiomas que ha nombrado. offsetof() a que me gusta tu offsetof() , pero otros han notado que en C ++, hay tipos a los que no se puede aplicar offsetof . En C, por otro lado, el estándar describe específicamente su aplicación para estructurar tipos, pero no dice nada sobre tipos de unión. Su comportamiento en los tipos de unión, aunque es muy probable que sea consistente y natural, como técnicamente indefinido.

Solo en C, podría usar un literal compuesto para evitar el comportamiento indefinido en su enfoque:

#define HAS_MEMBER(T,X) (((T){0}).X, #X)

Eso funciona igual de bien en los tipos de estructura y unión (aunque debe proporcionar un nombre de tipo completo para esta versión, no solo una etiqueta). Su comportamiento está bien definido cuando el tipo dado tiene tal miembro. La expansión viola una restricción de idioma, lo que requiere que se emita un diagnóstico cuando el tipo no tiene dicho miembro, incluso cuando no es un tipo de estructura ni un tipo de unión.

También puede usar sizeof , como sugirió @alain, porque aunque se evaluará sizeof expression, su operando no se evaluará (excepto en C, cuando su operando tiene un tipo modificado de forma variable, que no se aplicará a su uso). Creo que esta variación funcionará tanto en C como en C ++ sin introducir ningún comportamiento indefinido:

#define HAS_MEMBER(T,X) (sizeof(((T *)NULL)->X), #X)

Lo he escrito nuevamente para que funcione tanto para estructuras como para uniones.