todas programacion orientada objetos las funciones ejemplos codigos codigo clases c++ inline translation-unit

c++ - programacion - ¿Por qué la definición de la función global en línea en 2 archivos cpp diferentes produce un resultado mágico?



programacion orientada a objetos c++ ejemplos (4)

Como han señalado otros, los compiladores cumplen con el estándar de C ++ porque la regla de Una definición establece que solo tendrá una definición de una función, excepto si la función está en línea, entonces las definiciones deben ser las mismas.

En la práctica, lo que sucede es que la función está marcada como en línea, y en la etapa de enlace, si se ejecuta en múltiples definiciones de un token marcado en línea, el enlazador descarta todos menos uno. Si se ejecuta en varias definiciones de un token no marcado en línea, en su lugar genera un error.

Esta propiedad se denomina en inline porque, antes de LTO (optimización de tiempo de enlace), tomar el cuerpo de una función y "integrarlo" en el sitio de llamada requería que el compilador tuviera el cuerpo de la función. inline funciones en inline se pueden colocar en archivos de encabezado, y cada archivo cpp puede ver el cuerpo y "en línea" el código en el sitio de la llamada.

Esto no significa que el código esté realmente en línea; más bien, hace que sea más fácil para los compiladores integrarlo.

Sin embargo, no conozco un compilador que verifique que las definiciones sean idénticas antes de descartar duplicados. Esto incluye compiladores que, de otro modo, verifican que las definiciones de los cuerpos de funciones sean idénticas, como el plegado COMDAT de MSVC. Esto me entristece, porque es un conjunto realmente sutil de errores.

La forma correcta de solucionar su problema es colocar la función en un espacio de nombres anónimo. En general, debe considerar colocar todo en un archivo de origen en un espacio de nombres anónimo.

Otro ejemplo realmente desagradable de esto:

// A.cpp struct Helper { std::vector<int> foo; Helper() { foo.reserve(100); } }; // B.cpp struct Helper { double x, y; Helper():x(0),y(0) {} };

Los métodos definidos en el cuerpo de una clase están implícitamente en línea . Se aplica la regla ODR. Aquí tenemos dos Helper::Helper() , ambas en línea, y difieren.

Los tamaños de las dos clases difieren. En un caso, inicializamos dos sizeof(double) con 0 (ya que el flotador cero es cero bytes en la mayoría de las situaciones).

En otra, primero inicializamos tres sizeof(void*) con cero, luego llamamos a .reserve(100) en esos bytes interpretándolos como un vector.

En el momento del enlace, una de estas dos implementaciones es descartada y utilizada por la otra. Qué más, el que se descarta es probable que sea bastante determinístico en una compilación completa. En una construcción parcial, podría cambiar el orden.

Así que ahora tiene un código que puede compilar y funcionar "bien" en una compilación completa, pero una compilación parcial provoca daños en la memoria. Y cambiar el orden de los archivos en los makefiles puede causar daños en la memoria, o incluso cambiar el orden en que se vinculan los archivos, o actualizar su compilador, etc.

Si ambos archivos cpp tuvieran un bloque de namespace {} que contenga todo excepto las cosas que está exportando (que pueden usar nombres de espacios de nombres completos), esto no podría suceder.

He capturado exactamente este error en la producción varias veces. Dado lo sutil que es, no sé cuántas veces se deslizó, esperando a que llegue el momento de saltar.

Supongamos que tengo dos archivos file1.cpp y file2.cpp :

// file1.cpp #include <iostream> inline void foo() { std::cout << "f1/n"; } void f1() { foo(); }

y

// file2.cpp #include <iostream> inline void foo() { std::cout << "f2/n"; } void f2() { foo(); }

Y en main.cpp he reenviado el f1() y f2() :

void f1(); void f2(); int main() { f1(); f2(); }

Resultado ( no depende de la compilación, el mismo resultado para las compilaciones de depuración / lanzamiento ):

f1 f1

Whoa: El compilador de alguna manera solo selecciona la definición de file1.cpp y la usa también en f2() . ¿Cuál es la explicación exacta de este comportamiento?

Tenga en cuenta que cambiar de inline a static es una solución para este problema. Poner la definición en línea dentro de un espacio de nombres sin nombre también resuelve el problema y el programa se imprime:

f1 f2


El compilador puede suponer que todas las definiciones de la misma función en inline son idénticas en todas las unidades de traducción porque el estándar lo dice. Así que puede elegir cualquier definición que quiera. En tu caso, esa fue la que tiene f1 .

Tenga en cuenta que no puede confiar en que el compilador siempre elija la misma definición, ya que violar la regla mencionada hace que el programa no esté bien formado. El compilador también podría diagnosticar eso y cometer errores.

Si la función es static o en un espacio de nombres anónimo, tiene dos funciones distintas llamadas foo y el compilador debe seleccionar la que se encuentra en el archivo correcto.

Norma relevante para referencia:

Una función en línea se definirá en cada unidad de traducción en la que se utiliza odr y tendrá exactamente la misma definición en cada caso (3.2) . [...]

7.1.2 / 4 en N4141, enfatiza el mío.


Este es un comportamiento indefinido, porque las dos definiciones de la misma función en línea con enlace externo rompen el requisito de C ++ para los objetos que se pueden definir en varios lugares, conocidos como Regla de una definición :

3.2 Una regla de definición

...

  1. Puede haber más de una definición de un tipo de clase (Cláusula 9), tipo de enumeración (7.2), función en línea con enlace externo (7.1.2), plantilla de clase (Cláusula 14), [...] en un programa siempre que cada definición aparece en una unidad de traducción diferente, y siempre que las definiciones cumplan con los siguientes requisitos. Dada una entidad llamada D definida en más de una unidad de traducción, entonces

6.1 cada definición de D consistirá en la misma secuencia de tokens; [...]

Este no es un problema con las funciones static , porque una regla de definición no se aplica a ellas: C ++ considera que las funciones static definidas en diferentes unidades de traducción son independientes entre sí.


PUNTO DE CLARIFICACIÓN:

Aunque la respuesta arraigada en la regla en línea de C ++ es correcta, solo se aplica si ambas fuentes se compilan juntas. Si se compilan por separado, entonces, como señaló un comentarista, cada archivo de objeto resultante contendría su propio ''foo ()''. SIN EMBARGO: si estos dos archivos de objetos se vinculan, entonces como ambos ''foo ()'' - s no son estáticos, el nombre ''foo ()'' aparece en la tabla de símbolos exportados de ambos archivos de objetos; luego el enlazador debe unir las dos entradas de la tabla, por lo tanto, todas las llamadas internas se vuelven a vincular a una de las dos rutinas (probablemente la que se procesó en el primer archivo de objetos, ya que ya está enlazada [es decir, el vinculador trataría el segundo registro como ''extern'' independientemente de la vinculación]).