Soy consciente de cómo ODR, vinculación, static y extern "C" funcionan con funciones. Pero no estoy seguro de la visibilidad de los tipos, ya que no se pueden declarar static y no hay espacios de nombres anónimos en C.
En particular, me gustaría saber la validez del siguiente código si se compila como C y C++
// A.{c,cpp} typedef struct foo_t{ int x; int y; } Foo; static int use_foo() { Foo f; fx=5; return fx; } // B.{c,cpp} typedef struct foo_t{ double x; } Foo; static int use_foo() { Foo f; fx=5.0; return fx;// Cast on purpose }usando los siguientes dos comandos (sé que ambos compiladores detectan automáticamente el idioma en función de las extensiones, de ahí los diferentes nombres).
g++ -std=c++17 -pedantic -Wall -Wextra a.cpp b.cppgcc -std=c11 -pedantic -Wall -Wextra ac bcLas versiones 8.3 felizmente compilan ambos sin errores. Claramente, si ambos símbolos de estructura tienen enlaces externos, existe una violación de ODR porque las definiciones no son idénticas. Sí, el compilador no está obligado a informarlo, de ahí mi pregunta porque tampoco lo hizo.
No lo creo, para eso están los namespaces anónimos.
No estoy seguro aquí, he leído que los tipos se consideran static , lo que haría que el programa fuera válido. ¿Puede alguien confirmarlo?
Si estas definiciones estuvieran en archivos de encabezado públicos, tal vez en diferentes bibliotecas de C, y un programa de C++ incluye ambos, cada uno también en una TU diferente, ¿sería eso ODR? ¿Cómo se puede prevenir esto? ¿ extern "C" juega algún papel?
Para C. El programa es válido. El único requisito que se aplica aquí es la "regla de alias estricta" que dice que solo se puede acceder al objeto a través de un valor l de un tipo compatible (+ algunas excepciones descritas en 6.5p7 ).
La compatibilidad de estructuras/uniones definidas en unidades de traducción separadas se define en 6.2.7p1 .
... dos estructuras, uniones o tipos enumerados declarados en unidades de traducción separadas son compatibles si sus etiquetas y miembros cumplen los siguientes requisitos: si uno se declara con una etiqueta, el otro se declarará con la misma etiqueta. Si ambos se completan en cualquier lugar dentro de sus respectivas unidades de traducción, entonces se aplican los siguientes requisitos adicionales: debe haber una correspondencia uno a uno entre sus miembros de modo que cada par de miembros correspondientes se declaren con tipos compatibles; si un miembro del par se declara con un especificador de alineación, el otro se declara con un especificador de alineación equivalente; y si uno de los miembros de la pareja se declara con nombre, el otro se declara con el mismo nombre. Para dos estructuras, los miembros correspondientes se declararán en el mismo orden. Para dos estructuras o uniones, los campos de bits correspondientes tendrán el mismo ancho. Para dos enumeraciones, los miembros correspondientes tendrán los mismos valores.
Por lo tanto, las estructuras no son compatibles en el ejemplo.
Sin embargo, no es un problema porque el objeto f se crea y se accede a través de un tipo definido localmente. Se invocaría UB si el objeto se creó con el tipo Foo definido en una unidad de traducción y se accedió a través de otro tipo Foo en la otra unidad de traducción:
// Ac typedef struct foo_t{ int x; int y; } Foo; void bar(void *f); void foo() { Foo f; bar(&f); } // Bc typedef struct foo_t{ double x; } Foo; // using void* to avoid passing pointer to incompatible types void bar(void *f_) { Foo *f = f_; f->x=5.0; // UB! }Usaré como referencia el borrador n1570 para C11 para el lenguaje C y el borrador n4860 para C++20 para el lenguaje C++.
lenguaje C
Los tipos no tienen enlace en C: 6.2.2 Enlaces de identificadores §6:
Los siguientes identificadores no tienen vinculación: un identificador declarado como cualquier cosa que no sea un objeto o una función...
Eso significa que los tipos utilizados en ac y bc no están relacionados: declara correctamente diferentes objetos en ambas unidades de compilación.
lenguaje C++
Los tipos tienen vinculación en C++. 6.6 Programa y vinculación [basic.link] dice (énfasis mío):
Se dice que un nombre tiene vinculación cuando puede denotar el mismo objeto, referencia, función, tipo , plantilla, espacio de nombres o valor que un nombre introducido por una declaración en otro ámbito.
Un espacio de nombres sin nombre o un espacio de nombres declarado directa o indirectamente dentro de un espacio de nombres sin nombre tiene una vinculación interna. Todos los demás espacios de nombres tienen enlaces externos . Un nombre que tiene un ámbito de espacio de nombres al que no se le ha dado un vínculo interno anteriormente y que es el nombre de
...
una clase con nombre...
...
tiene su vinculación determinada de la siguiente manera:
— si el espacio de nombres adjunto tiene un enlace interno, el nombre tiene un enlace interno;
— de lo contrario, si la declaración del nombre se adjunta a un módulo con nombre (10.1) y no se exporta (10.2), el nombre tiene enlace de módulo;
— de lo contrario, el nombre tiene enlace externo
Los tipos declarados en a.cpp y b.cpp comparten el mismo identificador con enlace externo y no son compatibles: el programa está mal formado.
Dicho esto, la mayoría de los compiladores comunes pueden compilar fuentes C o C ++, y apostaría una moneda a que se esfuerzan por compartir la mayor parte de la implementación de ambos lenguajes. Por esa razón, confiaría en la implementación del mundo real para producir los resultados esperados incluso para el lenguaje C++. Pero Undefined Behavior no prohíbe los resultados esperados...
Otras respuestas señalan que este es un programa mal formado en C++.
En la práctica, los errores de enlace en funciones sobrecargadas serían posibles si tiene dos definiciones separadas de (no estático) void foo(bar); en unidades de traducción separadas. Espero que esto sea (parte de) por qué C++ tiene esta regla de que (algunos) tipos tienen enlaces externos.
Si los tipos fueran verdaderamente privados, no entrarían en conflicto. Pero cambiarán el nombre de la misma manera, porque si ambas TU tienen la misma definición de la bar de tipos (por ejemplo, a través de un .h o una copia manual), deben resolver llamar a la misma función.
// A.cpp typedef struct foo{ // names ending with _t are reserved int x; int y; } Foo; int take_foo(Foo f) { return fx; } int main(){} // so it's linkable without special options like -nostdlib and linker entry-point defaults // B.{c,cpp} typedef struct foo{ double x; } Foo; double take_foo(Foo f) { return fx; } En caso de que sea importante, estas funciones se compilarán en diferentes códigos de máquina en algunos objetivos, incluido x86-64 System V ABI donde lo probé. (El primer argumento double ya está en el registro de valor de retorno, incluso si está dentro de una estructura que contiene solo un par de dobles. Pero a diferencia de ARM64 y algunos otros RISC, el primer argumento entero no se pasa en el registro de valor de retorno, por lo que un mov se requiere antes del ret .)
$ g++ [AB].cpp /usr/bin/ld: /tmp/ccM89kvx.o: in function `take_foo(foo)': B.cpp:(.text+0x0): multiple definition of `take_foo(foo)'; /tmp/cckZ5qRG.o:A.cpp:(.text+0x0): first defined here collect2: error: ld returned 1 exit statusNo hay error si las funciones o las etiquetas de estructura tienen nombres diferentes. (Y sí, compilé con la optimización deshabilitada y sin optimización de tiempo de enlace, por lo que nada tuvo la oportunidad de eliminar las funciones no utilizadas antes de que entraran en conflicto).
Sin embargo, simplemente cambiar el nombre de typedef sin cambiar la etiqueta de estructura no es suficiente. Eso tiene sentido; todos los typedefs para el mismo tipo deben resolverse con el mismo nombre de asm, por lo que GCC mutila según la etiqueta struct, incluso si no la usa directamente. Tenga en cuenta los mensajes de error del enlazador que lo separan de nuevo a take_foo(foo) no a Foo .
No revisé la redacción estándar para ver si dos typedef ... Foo sería legal en ISO C++, a pesar de no ser un problema en la práctica para las implementaciones de C++ del mundo real.
Hacer que cualquiera de las funciones sea static también solucionaría el problema, porque está bien que las funciones static tengan el mismo nombre de asm.
Esto también tendría un error de vinculación si se compila como C, que no tiene sobrecarga de funciones, por lo que ya es un problema tener dos funciones take_foo no estáticas en el mismo programa, independientemente de que sus argumentos sean estructuras del mismo nombre de etiqueta o no. .