C - Accediendo a un non-const a través de una declaración const

Está accediendo a unconst objeto a través de un const declaración permitida por el estándar C? Por ejemplo, ¿se garantiza que el siguiente código compile y genere 23 y 42 en una plataforma que cumpla con los estándares?

unidad de traducción A:

int a = 23;
void foo(void) { a = 42; }    

unidad de traducción B:

#include <stdio.h>

extern volatile const int a;
void foo(void);

int main(void) {
    printf("%i\n", a);
    foo();
    printf("%i\n", a);
    return 0;
}

En la ISO / IEC 9899: 1999, acabo de encontrar (6.7.3, párrafo 5):

Si se intenta modificar un objeto definido con un tipo calificado const mediante el uso de un lvalue con un tipo no calificado const, el comportamiento es indefinido.

Pero en el caso anterior, el objeto no se define como const (pero recién declarado).

ACTUALIZACIÓN

Finalmente lo encontré en ISO / IEC 9899: 1999.

6.2.7, 2

Todas las declaraciones que se refieran al mismo objeto o función deberán tener un tipo compatible; de lo contrario, el comportamiento no está definido.

6.7.3, 9

Para que dos tipos calificados sean compatibles, ambos deberán tener la versión calificada idénticamente de un tipo compatible; [...]

Por lo tanto, is comportamiento indefinido.

preguntado el 08 de noviembre de 11 a las 14:11

Supongo que su ejemplo no se ajusta a los estándares, porque está utilizando algún truco de enlazador para lograr la conversión invisible de constante a no constante. -

+1 por encontrar la referencia que no pude. Mi respuesta debe leerse teniendo eso en cuenta. -

5 Respuestas

TU A contiene la (única) definición de a. Así que, a realmente es un objeto no constante, y se puede acceder a él como tal desde una función en A sin problemas.

Estoy bastante seguro de que TU B invoca un comportamiento indefinido, desde su declaración de a no está de acuerdo con la definición. La mejor cita que he encontrado hasta ahora para respaldar que esto es UB es 6.7.5 / 2:

Cada declarador declara un identificador y afirma que cuando un operando de la misma forma que el declarador aparece en una expresión, designa una función u objeto con el alcance, la duración del almacenamiento y el tipo indicado por los especificadores de declaración.

[Editar: el interrogador ha encontrado desde entonces la referencia adecuada en el estándar, vea la pregunta].

Aquí, la declaración en B afirma que a tiene tipo volatile const int. De hecho, el objeto no tiene tipo (calificado) volatile const int, tiene tipo (calificado) int. La violación de la semántica es UB.

En la práctica, lo que sucederá es que TU A se compilará como si a no es constante. TU B se compilará como si a eran un volatile const int, lo que significa que no almacenará en caché el valor de a en absoluto. Por lo tanto, esperaría que funcione siempre que el vinculador no se dé cuenta y se oponga a los tipos no coincidentes, porque no veo de inmediato cómo TU B podría emitir un código que sale mal. Sin embargo, mi falta de imaginación no es lo mismo que un comportamiento garantizado.

AFAIK, no hay nada en el estándar que diga eso volatile Los objetos en el ámbito del archivo no se pueden almacenar en un banco de memoria completamente diferente de otros objetos, lo que proporciona diferentes instrucciones para leerlos. La implementación aún tendría que ser capaz de leer un objeto normal a través de, digamos, un volatile puntero, así que suponga, por ejemplo, que la instrucción de carga "normal" funciona en objetos "especiales", y lo usa cuando lee a través de un puntero a un tipo calificado volátil. Pero si (como optimización) la implementación emitió la instrucción especial para objetos especiales, y la instrucción especial no trabajar en objetos normales, luego boom. Y creo que es culpa del programador, aunque confieso que inventé esta implementación hace solo 2 minutos, así que no puedo estar completamente seguro de que se ajuste.

respondido 09 nov., 11:18

+1 para la cita (aunque es una declaración bastante confusa). En cuanto a su último párrafo: Esto es lo que tenía en mente cuando surgió la pregunta. Vi una plataforma donde acceder a consts utiliza un esquema de direccionamiento diferente al del acceso aconstsy me preguntaba si esto es legítimo. - undur_gongor

Creo que es legítimo, teniendo en cuenta (como dije en mi respuesta) que tiene que ser posible acceder a objetos no constantes a través de un puntero a constante. Por lo tanto, la implementación debe distinguir entre el acceso a través de un puntero C y el acceso mediante el nombre de la variable. - Steve Jessop

No creo que una declaración pueda provocar UB, solo el acceso al objeto podría hacerlo. Pero 6.7.3 / 2 estados "Las propiedades asociadas con tipos calificados son significativas solo para expresiones que son lvalues". Desde aqui a solo está presente en una declaración y como expresión rvalue debería permitirse. - Jens Gustedt

@Jens: en C ++ es fácil, la expresión a es un lvalue, al que se aplica la conversión de lvalue a rvalue. No recuerdo cuál es el equivalente en C. ¿Por qué decir que la declaración por sí sola no puede provocar UB, si viola una restricción semántica? Bien, en la práctica sabemos que las implementaciones no emiten ningún código para una declaración, y que sin código un programa no "saldrá mal" en tiempo de ejecución, pero seguramente eso es un detalle de implementación. Para una instancia que no funciona en tiempo de ejecución: si el vinculador detecta la discrepancia y se niega a vincular, ¿está diciendo que la implementación no es conforme? - Steve Jessop

En la unidad de traducción B, const solo prohibiría modificar el a variable dentro de la propia unidad de traducción B.

Las modificaciones de ese valor desde el exterior (otras unidades de traducción) se reflejarán en el valor que ve en B.

Este es más un problema de vinculador que un problema de idioma. El enlazador es libre de desaprobar las diferentes calificaciones del a símbolo (si existe tal información en los archivos objeto) al fusionar las unidades de traducción compiladas.

Sin embargo, tenga en cuenta que si es al revés (const int a = 23 en A y extern int a en B), es probable que encuentre una infracción de acceso a la memoria en caso de intentar modificar a de B, ya que a podría colocarse en un área de solo lectura del proceso, generalmente mapeado directamente desde el .rodata sección del ejecutable.

respondido 08 nov., 11:19

Gracias. Eso es lo que sospechaba. Pero aún así prefiero tener una justificación del estándar o una fuente similar ... en particular porque otros desarrolladores parecen no estar de acuerdo ;-) - undur_gongor

@undur_gongor: lo que estás preguntando es en realidad un problema de creación de enlaces, no uno relacionado con el idioma. Este conflicto solo puede ser encontrado por el enlazador, después de que se hayan compilado las dos unidades de traducción. - Blagovest Buyukliev

¿No está el enlace también cubierto por el estándar C? - undur_gongor

@undur_gongor: No lo creo. La vinculación es específica de la implementación. El estándar solo habla de vínculos externos e internos. - Blagovest Buyukliev

Los detalles de cómo ocurre la vinculación dependen en gran medida de la implementación. Pero, por supuesto, el enlazador es parte de la implementación, por lo que todo lo que haga debe ajustarse al comportamiento requerido del programa (si lo hay, en este caso no creo que haya ningún comportamiento requerido). - Steve Jessop

La declaración que tiene la inicialización es la definición, por lo que su objeto no es un const objeto calificado y foo tiene todos los derechos para modificarlo.

En B, estás proporcionando acceso a ese objeto que tiene la const calificación. Dado que los tipos (el const versión calificada y la versión no calificada) tienen la misma representación de objeto, el acceso de lectura a través de ese identificador es válido.

Tu segundo printf, sin embargo, tiene un problema. Dado que no calificó su versión B de a as volatile no tiene garantía de ver la modificación de a. El compilador puede optimizar y reutilizar el valor anterior que pudo haber mantenido en un registro.

respondido 08 nov., 11:19

El último párrafo es incorrecto en mi humilde opinión. Una llamada a función es un punto de secuencia, y si una variable global se puede 'almacenar en caché' sobre una llamada a función, muchos programas fallarían. Entonces, volátil no es necesario ni útil. - Johan Bezem

@JohanBezem: Pero esta no es una variable regular sino una const. Creo que el compilador puede asumir que es constante. - undur_gongor

La variable es externa y en este contexto constante. Eso no significa que sea constante en todas partes, por lo que en mi humilde opinión, el compilador no puede optimizar a través de la llamada a foo(). - Johan Bezem

@JohanBezem, no estoy seguro, pero ser un punto de secuencia probablemente no sea suficiente como argumento. Entonces, cada declaración completa obligaría al compilador a recargar todas las variables. Una llamada a función impone más que eso, ya que el compilador no puede deducir nada sobre el estado de los objetos externos después de tal llamada. Por otro lado, la declaración es fingiendo que el objeto no es modificable, por lo que se podría argumentar que podemos asumir que nunca se modifica. Esto quizás sería diferente si tuviéramos un puntero a un const, pero aquí la declaración establece que el objeto en sí es así. - Jens Gustedt

@Jens: sin volátiles, creo que tienes razón, el código en TU B puede leer a una vez y nunca más. - Steve Jessop

Declararlo como const significa que la instancia se define como const. No puede acceder a él desde un not-const. La mayoría de los compiladores no lo permiten, y el estándar dice que tampoco está permitido.

respondido 08 nov., 11:18

Gracias. ¿Tiene alguna referencia para respaldar esto? Con gcc 4.6.1 en mingw386, el código funciona como se describe. - undur_gongor

La única referencia que tengo es la que citó de todos modos. - Polinomio

FWIW: En H & S5 está escrito (Sección 4.4.3 Calificadores de tipo, página 89): "Cuando se usa en un contexto que requiere un valor en lugar de un designador, los calificadores se eliminan del tipo". Entonces el const solo tiene efecto cuando alguien intenta escribir algo en la variable. En este caso, el printfuso a como un valor r, y el agregado volatile (innecesario en mi humilde opinión) hace que el programa lea la variable de nuevo, así que yo diría que el programa es necesario para producir la salida que vio el OP inicialmente, en todas las plataformas / compiladores. Veré el estándar y lo agregaré si / cuando encuentre algo nuevo.

EDITAR: No pude encontrar ninguna solución definitiva a esta pregunta en el Estándar (utilicé el último borrador para C1X), ya que todas las referencias al comportamiento del enlazador se concentran en que los nombres sean idénticos. Los calificadores de tipo en declaraciones externas no parecen estar cubiertos. Quizás deberíamos enviar esta pregunta al Comité de Normas C.

respondido 08 nov., 11:22

I pensar esto significa que los calificadores se eliminan del tipo de valor (y, por lo tanto, del tipo de subexpresión que produce ese valor), no que se eliminan del tipo de cualquier objeto desde el que se haya cargado ese valor . Asique pensar sigue siendo un problema que el objeto designado por a en TU A tiene un tipo diferente del objeto supuestamente designado por a en TU B. Pero admito que no he encontrado un estándar perfectamente claro para apoyar mi respuesta. - Steve Jessop

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.