Las constantes de enumeración se comportan de manera diferente en C y C ++

81

Por que hace esto:

#include <stdio.h>
#include <limits.h>
#include <inttypes.h>

int main() {
    enum en_e {
        en_e_foo,
        en_e_bar = UINT64_MAX,
    };
    enum en_e e = en_e_foo;
    printf("%zu\n", sizeof en_e_foo);
    printf("%zu\n", sizeof en_e_bar);
    printf("%zu\n", sizeof e);
}

imprimir 4 8 8en C y 8 8 8en C ++ (en una plataforma con entradas de 4 bytes)?

Tenía la impresión de que la UINT64_MAXasignación forzaría todas las constantes de enumeración a al menos 64 bits, pero en_e_foopermanece en 32 en C.

¿Cuál es el motivo de la discrepancia?

PSkocik
fuente
1
¿Qué compiladores? No sé si hace una diferencia, pero podría.
Mark Ransom
@MarkRansom Se le ocurrió gcc pero clang se comporta igual.
PSkocik
Ejemplo en vivo de C
Drew Dormann
3
"en una plataforma con entradas de 4 bytes" No es solo la plataforma, sino el compilador lo que determina los anchos de tipo. Eso puede ser todo lo que es. (Según la respuesta de Keith, en realidad no lo es, pero tenga en cuenta esas posibilidades en general)
Lightness Races in Orbit
1
@PSkocik: No es realmente un cambio, sólo que esta pregunta se encontró un uso válido de ambos c y C ++ (preguntar por qué cierto código provoca un comportamiento diferente entre los dos). También está bien: preguntar cómo llamar a bibliotecas C desde C ++, y cómo escribir C ++ que se pueda llamar desde C. Muy mal: hacer una pregunta C y lanzar una etiqueta C ++ en "para que tenga más ojos". Tampoco está bien: hacer una pregunta de C ++ y como una ocurrencia tardía "asegúrese de responder también por C". (y para los quejosos habituales, muy mal: cambiar una etiqueta C ++ a una etiqueta C porque el código usa funciones que existen en ambos estándares)
Ben Voigt

Respuestas:

80

En C, una enumconstante es de tipo int. En C ++, es del tipo enumerado.

enum en_e{
    en_e_foo,
    en_e_bar=UINT64_MAX,
};

En C, esto es una violación de la restricción , que requiere un diagnóstico ( si se UINT64_MAX excede INT_MAX, lo que muy probablemente lo haga). El compilador AC puede rechazar el programa por completo, o puede imprimir una advertencia y luego generar un ejecutable cuyo comportamiento no está definido. (No está 100% claro que un programa que viola una restricción necesariamente tenga un comportamiento indefinido, pero en este caso el estándar no dice cuál es el comportamiento, por lo que sigue siendo un comportamiento indefinido).

gcc 6.2 no advierte sobre esto. clang lo hace. Este es un error en gcc; inhibe incorrectamente algunos mensajes de diagnóstico cuando se utilizan macros de encabezados estándar. Gracias a Grzegorz Szpetkowski por localizar el informe de error: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71613

En C ++, cada tipo de enumeración tiene un tipo subyacente , que es un tipo entero (no necesariamente int). Este tipo subyacente debe poder representar todos los valores constantes. Entonces, en este caso, ambos en_e_fooy en_e_barson del tipo en_e, que debe tener al menos 64 bits de ancho, incluso si intes más estrecho.

Keith Thompson
fuente
10
nota rápida: para UINT64_MAXno exceder INT_MAXrequiere que intsea ​​al menos 65 bits.
Ben Voigt
10
Lo realmente extraño es que gcc (5.3.1) emite una advertencia con -Wpedanticy 18446744073709551615ULLpero no con UINT64_MAX.
nwellnhof
4
@dascandy: No, intdebe ser un tipo con signo, por lo que tendría que tener al menos 65 bits para poder representar UINT64_MAX(2 ** 64-1).
Keith Thompson
1
@KeithThompson, 6.7.2.2 dice que "los identificadores en una lista de enumeradores se declaran como constantes que tienen el tipo int y pueden aparecer donde estén permitidos". Tengo entendido que las constantes que declara una sola enumeración de C no usan el tipo de enumeración, por lo que a partir de ahí no es muy difícil convertirlas en tipos diferentes (especialmente si se implementa como una extensión del estándar).
zneak
2
@AndrewHenle: en_e_barno es más grande que la enumeración, en_e_fooes más pequeño. La variable enum era tan grande como la constante más grande.
Ben Voigt
25

Ese código simplemente no es válido en C en primer lugar.

La sección 6.7.2.2 tanto en C99 como en C11 dice que:

Limitaciones:

La expresión que de fi ne el valor de una constante de enumeración será una expresión de constante entera que tiene un valor representable como un int.

Un diagnóstico del compilador es obligatorio porque es una violación de la restricción, consulte 5.1.1.3:

Una implementación conforme producirá al menos un mensaje de diagnóstico (identificado de una manera definida por la implementación) si una unidad de traducción o unidad de traducción en proceso previo contiene una violación de cualquier regla o restricción de sintaxis, incluso si el comportamiento también se especifica explícitamente como indefinido o implementación. definido.

Ben Voigt
fuente
23

En C , mientras que a enumse considera un tipo separado, los enumeradores en sí siempre tienen tipo int.

C11 - 6.7.2.2 Especificadores de enumeración

3 Los identificadores en una lista de enumeradores se declaran como constantes que tienen tipo int ...

Por lo tanto, el comportamiento que ve es una extensión del compilador.

Yo diría que tiene sentido expandir el tamaño de uno de los enumeradores solo si su valor es demasiado grande.


Por otro lado, en C ++ todos los enumeradores tienen el tipo en el enumque están declarados.

Por eso, el tamaño de cada enumerador debe ser el mismo. Entonces, el tamaño de todo enumse expande para almacenar el enumerador más grande.

SantoNegroGato
fuente
11
Es una extensión del compilador, pero el hecho de no generar un diagnóstico es una no conformidad.
Ben Voigt
16

Como señalaron otros, el código está mal formado (en C), debido a la violación de la restricción.

Hay un error de GCC # 71613 (informado en junio de 2016), que indica que algunas advertencias útiles se silencian con macros.

Las advertencias útiles parecen silenciarse cuando se utilizan macros de los encabezados del sistema. Por ejemplo, en el ejemplo siguiente, una advertencia sería útil para ambas enumeraciones, pero solo se muestra una advertencia. Probablemente pueda suceder lo mismo con otras advertencias.

La solución actual puede ser anteponer la macro con un +operador unario :

enum en_e {
   en_e_foo,
   en_e_bar = +UINT64_MAX,
};

que produce un error de compilación en mi máquina con GCC 4.9.2:

$ gcc -std=c11 -pedantic-errors -Wall main.c 
main.c: In function ‘main’:
main.c:9:20: error: ISO C restricts enumerator values to range ofint’ [-Wpedantic]
         en_e_bar = +UINT64_MAX
Grzegorz Szpetkowski
fuente
12

C11 - 6.7.2.2/2

La expresión que define el valor de una constante de enumeración será una expresión de constante entera que tiene un valor representable como un int.

en_e_bar=UINT64_MAXes una infracción de restricción y esto invalida el código anterior. Se debe producir un mensaje de diagnóstico confirmando la implementación como se indica en el borrador C11:

Una implementación conforme producirá al menos un mensaje de diagnóstico (identificado de una manera definida por la implementación) si una unidad de traducción o unidad de traducción en proceso previo contiene una violación de cualquier regla o restricción de sintaxis, [...]

Parece que GCC tiene algún error y no pudo producir el mensaje de diagnóstico. (El error es señalado en la respuesta por Grzegorz Szpetkowski

haccks
fuente
8
"comportamiento indefinido" es un efecto de tiempo de ejecución. sizeofes un operador en tiempo de compilación. No hay UB aquí, e incluso si lo hubiera, no podría afectar sizeof.
Ben Voigt
2
Debería encontrar la cita estándar de que los enumeradores que no pueden caber en un int son UB. Soy muy escéptico de esa declaración y mi voto se mantendrá firme en -1 hasta que esto se aclare.
zneak
3
@Sergey: El estándar C en realidad dice "La expresión que define el valor de una constante de enumeración debe ser una expresión de constante entera que tiene un valor representable como un int". pero violar esto sería una violación de restricción, se requiere diagnóstico, no UB.
Ben Voigt
3
@haccks: ¿Sí? Es una violación de la restricción y "Una implementación conforme producirá al menos un mensaje de diagnóstico (identificado de una manera definida por la implementación) si una unidad de traducción o unidad de traducción en proceso previo contiene una violación de cualquier regla o restricción de sintaxis, incluso si el comportamiento también es explícitamente especificado como indefinido o definido por implementación ".
Ben Voigt
2
Existe una diferencia entre desbordamiento y truncamiento. El desbordamiento es cuando tiene una operación aritmética que produce un valor demasiado grande para el tipo de resultado esperado y el desbordamiento con signo es UB. El truncamiento es cuando tiene un valor que era demasiado grande para que el tipo de destino comenzara (como short s = 0xdeadbeef) y el comportamiento está definido por la implementación.
zneak
5

Eché un vistazo a los estándares y mi programa parece ser una violación de restricción en C debido a 6.7.2.2p2 :

Restricciones: la expresión que define el valor de una constante de enumeración debe ser una expresión de constante entera que tiene un valor representable como un int.

y definido en C ++ debido a 7.2.5:

Si el tipo subyacente no es fijo, el tipo de cada enumerador es el tipo de su valor de inicialización: - Si se especifica un inicializador para un enumerador, el valor de inicialización tiene el mismo tipo que la expresión y la expresión-constante debe ser una integral expresión constante (5.19). - Si no se especifica ningún inicializador para el primer enumerador, el valor de inicialización tiene un tipo integral no especificado. - De lo contrario, el tipo del valor de inicialización es el mismo que el tipo del valor de inicialización del enumerador anterior, a menos que el valor incrementado no sea representable en ese tipo, en cuyo caso el tipo es un tipo integral no especificado suficiente para contener el valor incrementado. Si no existe tal tipo, el programa está mal formado.

PSkocik
fuente
3
No está "indefinido" en C, está "mal formado" porque se viola una restricción. El compilador DEBE generar un diagnóstico sobre la infracción.
Ben Voigt
@BenVoigt Gracias por enseñarme sobre la diferencia. Lo solucioné en la respuesta (que hice porque me perdí una cita del estándar C ++ en las otras respuestas).
PSkocik