¿Es seguro vincular objetos C ++ 17, C ++ 14 y C ++ 11?

103

Supongamos que tengo tres objetos compilados, todos producidos por el mismo compilador / versión :

  1. A fue compilado con el estándar C ++ 11
  2. B fue compilado con el estándar C ++ 14
  3. C fue compilado con el estándar C ++ 17

Para simplificar, supongamos que todos los encabezados se escribieron en C ++ 11, utilizando solo construcciones cuya semántica no ha cambiado entre las tres versiones estándar , por lo que cualquier interdependencia se expresó correctamente con la inclusión del encabezado y el compilador no objetó.

¿Qué combinaciones de estos objetos es y no es seguro vincular en un solo binario? ¿Por qué?


EDITAR: las respuestas que cubren los principales compiladores (por ejemplo, gcc, clang, vs ++) son bienvenidas

ricab
fuente
6
No es una pregunta de la escuela / entrevista. La pregunta surge de un caso particular: estoy trabajando en un proyecto que depende de una biblioteca de código abierto. Construyo esta biblioteca desde la fuente, pero su sistema de construcción solo acepta una bandera para elegir entre la construcción C ++ 03 / C ++ 11. Sin embargo, el compilador que utilizo admite otros estándares y estoy considerando actualizar mi propio proyecto a C ++ 17. No estoy seguro de si es una decisión segura. ¿Puede haber una interrupción en el ABI o alguna otra forma en la que el enfoque no sea aconsejable? No encontré una respuesta clara y decidí publicar una pregunta sobre el caso general.
ricab
6
Esto depende completamente del compilador. No hay nada en las especificaciones formales de C ++ que gobierne esta situación. También existe una pequeña posibilidad de que el código escrito en los estándares C ++ 03 o C + 11 tenga algunos problemas en el nivel C ++ 14 y C ++ 17. Con suficiente conocimiento y experiencia (y un código bien escrito para empezar), debería ser posible solucionar cualquiera de estos problemas. Sin embargo, si no está muy familiarizado con los estándares de C ++ más nuevos, es mejor que se ciña a lo que admite el sistema de compilación y está probado para que funcione.
Sam Varshavchik
10
@Someprogrammerdude: Es una pregunta muy valiosa. Ojalá tuviera una respuesta. Todo lo que sé es que libstdc ++ a través de RHEL devtoolset es compatible con versiones anteriores por diseño, al vincular estáticamente las cosas más nuevas y dejar que las cosas más antiguas se resuelvan dinámicamente en tiempo de ejecución utilizando el libstdc ++ "nativo" de la distribución. Pero eso no responde a la pregunta.
Lightness Races in Orbit
4
@nm: ... que es principalmente el caso ... casi todos los que distribuyen bibliotecas C ++ independientes de la distribución lo hacen (1) en forma de biblioteca dinámica y (2) sin contenedores de biblioteca estándar C ++ en los límites de la interfaz. Las bibliotecas que provienen de una distribución de Linux lo tienen fácil, ya que todas están construidas con el mismo compilador, la misma biblioteca estándar y prácticamente el mismo conjunto de indicadores predeterminados.
Matteo Italia
3
Solo para aclarar el comentario anterior de @MatteoItalia "y al cambiar de modo C ++ 03 a C ++ 11 (std :: string en particular)". Esto no es cierto, la std::stringimplementación activa en libstdc ++ es independiente del -stdmodo utilizado . Esta es una propiedad importante, precisamente para soportar situaciones como las de los OP. Puede usar el std::stringcódigo nuevo en C ++ 03 y puede usar el std::stringcódigo antiguo en C ++ 11 (consulte el enlace en el comentario posterior de Matteo).
Jonathan Wakely

Respuestas:

123

¿Qué combinaciones de estos objetos es y no es seguro vincular en un solo binario? ¿Por qué?

Para GCC es seguro vincular cualquier combinación de objetos A, B y C. Si todos están construidos con la misma versión, entonces son compatibles con ABI, la versión estándar (es decir, la-std opción) no hace ninguna diferencia.

¿Por qué? Porque esa es una propiedad importante de nuestra implementación que trabajamos arduamente para asegurar.

Donde tiene problemas es si vincula objetos compilados con diferentes versiones de GCC y ha utilizado características inestables de un nuevo estándar C ++ antes de que se complete el soporte de GCC para ese estándar. Por ejemplo, si compila un objeto usando GCC 4.9 -std=c++11y otro objeto con GCC 5 -std=c++11, tendrá problemas. El soporte de C ++ 11 era experimental en GCC 4.x, por lo que hubo cambios incompatibles entre las versiones de GCC 4.9 y 5 de las características de C ++ 11. Del mismo modo, si compila un objeto con GCC 7 -std=c++17y otro objeto con GCC 8 y-std=c++17 tendrá problemas, porque el soporte de C ++ 17 en GCC 7 y 8 aún es experimental y está evolucionando.

Por otro lado, cualquier combinación de los siguientes objetos funcionará (aunque vea la nota a continuación sobre la libstdc++.soversión):

  • objeto D compilado con GCC 4.9 y -std=c++03
  • objeto E compilado con GCC 5 y -std=c++11
  • objeto F compilado con GCC 7 y -std=c++17

Esto se debe a que el soporte de C ++ 03 es estable en las tres versiones del compilador utilizadas, por lo que los componentes de C ++ 03 son compatibles entre todos los objetos. El soporte de C ++ 11 es estable desde GCC 5, pero el objeto D no usa ninguna característica de C ++ 11, y los objetos E y F usan versiones donde el soporte de C ++ 11 es estable. La compatibilidad con C ++ 17 no es estable en ninguna de las versiones del compilador utilizadas, pero solo el objeto F usa las características de C ++ 17 y por lo tanto no hay problemas de compatibilidad con los otros dos objetos (las únicas características que comparten provienen de C ++ 03 o C ++ 11, y las versiones utilizadas hacen que esas partes estén bien). Si luego quisiera compilar un cuarto objeto, G, usando GCC 8 y -std=c++17luego necesitaría recompilar F con la misma versión (o no vincularlo a F) porque los símbolos de C ++ 17 en F y G son incompatibles.

La única advertencia para la compatibilidad descrita anteriormente entre D, E y F es que su programa debe usar la libstdc++.sobiblioteca compartida de GCC 7 (o posterior). Debido a que el objeto F se compiló con GCC 7, debe usar la biblioteca compartida de esa versión, ya que compilar cualquier parte del programa con GCC 7 podría introducir dependencias en símbolos que no están presentes en libstdc++.soGCC 4.9 o GCC 5. De manera similar, si se vinculó al objeto G, construido con GCC 8, necesitaría usar el libstdc++.sode GCC 8 para asegurarse de que se encuentren todos los símbolos necesarios para G. La regla simple es asegurarse de que la biblioteca compartida que utiliza el programa en tiempo de ejecución sea al menos tan nueva como la versión utilizada para compilar cualquiera de los objetos.

Otra advertencia al usar GCC, ya mencionada en los comentarios de su pregunta, es que desde GCC 5 hay dos implementacionesstd::string disponibles en libstdc ++. Las dos implementaciones no son compatibles con enlaces (tienen diferentes nombres mutilados, por lo que no se pueden vincular entre sí) pero pueden coexistir en el mismo binario (tienen diferentes nombres mutilados, así que no entre en conflicto si un objeto usa std::stringy el otros usos std::__cxx11::string). Si sus objetos usan std::string, por lo general, todos deben compilarse con la misma implementación de cadena. Compile con -D_GLIBCXX_USE_CXX11_ABI=0para seleccionar la gcc4-compatibleimplementación original o -D_GLIBCXX_USE_CXX11_ABI=1para seleccionar la nuevacxx11 implementación (no se deje engañar por el nombre, también se puede usar en C ++ 03, se llamacxx11porque cumple con los requisitos de C ++ 11). La implementación predeterminada depende de cómo se configuró GCC, pero el valor predeterminado siempre se puede anular en tiempo de compilación con la macro.

Jonathan Wakely
fuente
"porque compilar cualquier parte del programa con GCC 7 podría introducir dependencias de los símbolos presentes en libstdc ++. así que de GCC 4.9 o GCC 5" quiso decir que NO están presentes en GCC 4.9 o GCC 5, ¿verdad? ¿Esto también se aplica a los enlaces estáticos? Gracias por la información sobre la compatibilidad entre las versiones del compilador.
Hadi Brais
1
Me acabo de dar cuenta de una gran falla al ofrecer una recompensa por esta pregunta. 😂
Lightness Races in Orbit
4
@ricab Estoy 90% seguro de que la respuesta es la misma para Clang / libc ++, pero no tengo ni idea de MSVC.
Jonathan Wakely
1
Esta respuesta es estelar. ¿Está documentado en algún lugar que 5.0+ es estable para el 14/11?
Barry
1
No muy claramente o en un solo lugar. gcc.gnu.org/gcc-5/changes.html#libstdcxx y gcc.gnu.org/onlinedocs/libstdc++/manual/api.html#api.rel_51 declaran que el soporte de la biblioteca para C ++ 11 es completo (el idioma el soporte estaba completo antes, pero aún era "experimental"). El soporte de la biblioteca C ++ 14 todavía figura como experimental hasta 6.1, pero creo que en la práctica nada cambió entre 5.xy 6.x que afecte a ABI.
Jonathan Wakely
17

Hay dos partes en la respuesta. Compatibilidad a nivel de compilador y compatibilidad a nivel de enlazador. Empecemos por el primero.

supongamos que todos los encabezados se escribieron en C ++ 11

Usar el mismo compilador significa que se usarán el mismo encabezado de biblioteca estándar y los mismos archivos fuente (las partes asociadas con el compilador) independientemente del estándar C ++ de destino. Por lo tanto, los archivos de encabezado de la biblioteca estándar se escriben para que sean compatibles con todas las versiones de C ++ admitidas por el compilador.

Dicho esto, si las opciones del compilador utilizadas para compilar una unidad de traducción especifican un estándar de C ++ en particular, entonces las funciones que solo están disponibles en los estándares más nuevos no deberían ser accesibles. Esto se hace usando la __cplusplusdirectiva. Consulte el archivo de origen vectorial para ver un ejemplo interesante de cómo se usa. Del mismo modo, el compilador rechazará cualquier característica sintáctica ofrecida por las versiones más nuevas del estándar.

Todo eso significa que su suposición solo se puede aplicar a los archivos de encabezado que escribió. Estos archivos de encabezado pueden causar incompatibilidades cuando se incluyen en diferentes unidades de traducción dirigidas a diferentes estándares C ++. Esto se discute en el Anexo C del estándar C ++. Hay 4 cláusulas, solo discutiré la primera y mencionaré brevemente el resto.

C.3.1 Cláusula 2: convenciones léxicas

Las comillas simples delimitan un carácter literal en C ++ 11, mientras que son separadores de dígitos en C ++ 14 y C ++ 17. Suponga que tiene la siguiente definición de macro en uno de los archivos de encabezado C ++ 11 puros:

#define M(x, ...) __VA_ARGS__

// Maybe defined as a field in a template or a type.
int x[2] = { M(1'2,3'4) };

Considere dos unidades de traducción que incluyen el archivo de encabezado, pero apuntan a C ++ 11 y C ++ 14, respectivamente. Al apuntar a C ++ 11, la coma entre las comillas no se considera un separador de parámetros; solo hay un parámetro. Por tanto, el código sería equivalente a:

int x[2] = { 0 }; // C++11

Por otro lado, al apuntar a C ++ 14, las comillas simples se interpretan como separadores de dígitos. Por tanto, el código sería equivalente a:

int x[2] = { 34, 0 }; // C++14 and C++17

El punto aquí es que el uso de comillas simples en uno de los archivos de encabezado de C ++ 11 puros puede resultar en errores sorprendentes en las unidades de traducción que apuntan a C ++ 14/17. Por lo tanto, incluso si un archivo de encabezado está escrito en C ++ 11, debe escribirse con cuidado para garantizar que sea compatible con versiones posteriores del estándar. La __cplusplusdirectiva puede resultar útil aquí.

Las otras tres cláusulas del estándar incluyen:

C.3.2 Cláusula 3: conceptos básicos

Cambio : nuevo desasignador habitual (sin ubicación)

Justificación : Requerido para desasignación de tamaño.

Efecto en la característica original : el código válido de C ++ 2011 podría declarar una función de asignación de ubicación global y una función de desasignación de la siguiente manera:

void operator new(std::size_t, std::size_t); 
void operator delete(void*, std::size_t) noexcept;

En esta Norma Internacional, sin embargo, la declaración de eliminación de operador puede coincidir con una eliminación de operador habitual predefinida (sin ubicación) (3.7.4). Si es así, el programa está mal formado, como lo fue para las funciones de asignación de miembros de clase y las funciones de desasignación (5.3.4).

C.3.3 Cláusula 7: declaraciones

Cambio : las funciones miembro no estáticas constexpr no son funciones miembro const implícitamente.

Justificación : Necesario para permitir que las funciones del miembro constexpr muten el objeto.

Efecto sobre la característica original : Es posible que el código C ++ 2011 válido no se compile en esta Norma Internacional.

Por ejemplo, el siguiente código es válido en C ++ 2011 pero no válido en esta Norma Internacional porque declara la misma función miembro dos veces con diferentes tipos de retorno:

struct S {
constexpr const int &f();
int &f();
};

C.3.4 Cláusula 27: biblioteca de entrada / salida

Cambio : obtiene no está definido.

Justificación : El uso de gets se considera peligroso.

Efecto sobre la característica original : el código válido de C ++ 2011 que usa la función gets puede fallar al compilar en esta Norma Internacional.

Las posibles incompatibilidades entre C ++ 14 y C ++ 17 se analizan en C.4. Dado que todos los archivos de encabezado no estándar están escritos en C ++ 11 (como se especifica en la pregunta), estos problemas no ocurrirán, por lo que no los mencionaré aquí.

Ahora discutiré la compatibilidad en el nivel del enlazador. En general, las posibles razones de las incompatibilidades incluyen las siguientes:

Si el formato del archivo de objeto resultante depende del estándar C ++ de destino, el vinculador debe poder vincular los diferentes archivos de objeto. En GCC, LLVM y VC ++, afortunadamente este no es el caso. Es decir, el formato de los archivos de objetos es el mismo independientemente del estándar de destino, aunque depende en gran medida del propio compilador. De hecho, ninguno de los enlazadores de GCC, LLVM y VC ++ requiere conocimientos sobre el estándar C ++ de destino. Esto también significa que podemos vincular archivos de objeto que ya están compilados (vinculando estáticamente el tiempo de ejecución).

Si la rutina de inicio del programa (la función que llama main) es diferente para diferentes estándares C ++ y las diferentes rutinas no son compatibles entre sí, entonces no sería posible vincular los archivos de objeto. En GCC, LLVM y VC ++, afortunadamente este no es el caso. Además, la firma de la mainfunción (y las restricciones que se le aplican, consulte la Sección 3.6 del estándar) es la misma en todos los estándares C ++, por lo que no importa en qué unidad de traducción exista.

En general, es posible que WPO no funcione bien con archivos de objeto compilados con diferentes estándares de C ++. Esto depende exactamente de qué etapas del compilador requieren conocimiento del estándar de destino y qué etapas no y el impacto que tiene en las optimizaciones entre procedimientos que cruzan archivos de objetos. Afortunadamente, GCC, LLVM y VC ++ están bien diseñados y no tienen este problema (no que yo sepa).

Por lo tanto, GCC, LLVM y VC ++ se han diseñado para permitir la compatibilidad binaria en diferentes versiones del estándar C ++. Sin embargo, esto no es realmente un requisito de la norma en sí.

Por cierto, aunque el compilador de VC ++ ofrece el modificador std , que le permite apuntar a una versión particular del estándar C ++, no es compatible con C ++ 11. La versión mínima que se puede especificar es C ++ 14, que es la predeterminada a partir de Visual C ++ 2013 Update 3. Podría usar una versión anterior de VC ++ para apuntar a C ++ 11, pero luego tendría que usar diferentes compiladores de VC ++ para compilar diferentes unidades de traducción que tengan como objetivo diferentes versiones del estándar C ++, lo que al menos rompería WPO.

AVISO: Mi respuesta puede no ser completa o muy precisa.

Hadi Brais
fuente
La pregunta realmente tenía que ver con la vinculación más que con la compilación. Reconozco (gracias a este comentario ) que quizás no estaba claro y lo he editado para dejar en claro que cualquier encabezado incluido tiene la misma interpretación en los tres estándares.
ricab
@ricab La respuesta cubre tanto la compilación como el enlace. Pensé que estabas preguntando por ambos.
Hadi Brais
1
De hecho, pero encuentro que la respuesta es demasiado larga y confusa, especialmente hasta "Ahora discutiré la compatibilidad en el nivel del enlazador". Puede reemplazar todo lo anterior con algo como si no se puede postular que los encabezados incluidos tengan el mismo significado en C ++ 11 y C ++ 14/17, entonces no es seguro incluirlos en primer lugar . Para la parte restante, ¿tiene una fuente que muestre que esos tres puntos son las únicas razones potenciales de incompatibilidad? Gracias por la respuesta en cualquier caso, sigo votando
ricab
@ricab No puedo decirlo con certeza. Es por eso que agregué la advertencia al final de la respuesta. Cualquier otra persona puede ampliar la respuesta para que sea más precisa o completa en caso de que me haya perdido algo.
Hadi Brais
Esto me confunde: "Usar el mismo compilador significa que se usarán los mismos archivos fuente y encabezado de biblioteca estándar (...)". ¿Cómo puede ser ese el caso? Si tengo un código antiguo compilado con gcc5, los 'archivos del compilador' que pertenecían a esa versión no pueden ser a prueba de futuro. Para el código fuente compilado en (tremendamente) diferentes momentos con diferentes versiones del compilador, podemos estar bastante seguros de que el encabezado de la biblioteca y los archivos fuente son diferentes. Con su regla de que estos deben ser iguales, debe recompilar el código fuente anterior con gcc5, ... y asegurarse de que todos usen los últimos (los mismos) 'archivos de compilación'.
user2943111
2

Los nuevos estándares de C ++ se dividen en dos partes: características del lenguaje y componentes de biblioteca estándar.

Como quiere decir con nuevo estándar , los cambios en el idioma en sí (por ejemplo, ranged-for) casi no hay problema (a veces existen conflictos en los encabezados de bibliotecas de terceros con características de idioma estándar más nuevas).

Pero la biblioteca estándar ...

Cada versión del compilador viene con una implementación de la biblioteca estándar C ++ (libstdc ++ con gcc, libc ++ con clang, biblioteca estándar MS C ++ con VC ++, ...) y exactamente una implementación, no muchas implementaciones para cada versión estándar. Además, en algunos casos, puede utilizar otra implementación de la biblioteca estándar que no sea la proporcionada por el compilador. Lo que debería importarle es vincular una implementación de biblioteca estándar más antigua con una más nueva.

El conflicto que podría ocurrir entre las bibliotecas de terceros y su código es la biblioteca estándar (y otras bibliotecas) que se vincula a esas bibliotecas de terceros.

E. Vakili
fuente
"Cada versión del compilador viene con una implementación de STL" No, no es así
Lightness Races in Orbit
@LightnessRacesinOrbit ¿Quiere decir que no hay relación entre, por ejemplo, libstdc ++ y gcc?
E. Vakili
9
No, me refiero a que el STL ha estado obsoleto de manera efectiva durante poco más de veinte años. Te refieres a la biblioteca estándar de C ++. En cuanto al resto de la respuesta, ¿puede proporcionar algunas referencias / pruebas para respaldar su afirmación? Creo que para una pregunta como esta es importante.
Lightness Races in Orbit
3
Lo siento, no, no está claro en el texto. Ha hecho algunas afirmaciones interesantes, pero aún no las ha respaldado con ninguna evidencia.
Lightness Races in Orbit