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


Supongamos que tengo tres objetos compilados, todos producidos por 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

Por simplicidad, supongamos que todos los encabezados fueron escritos 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 de 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


32
2017-10-14 16:36


origen


Respuestas:


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

Para GCC es seguro vincular juntas cualquier combinación de objetos A, B y C. Si todas están compiladas 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 duro para garantizar.

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

Por otro lado, cualquier combinación de los siguientes objetos funcionará (aunque vea la nota a continuación sobre libstdc++.so versió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 de 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. El soporte de C ++ 17 no es estable en ninguna de las versiones de compilador usadas, 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 quería compilar un cuarto objeto, G, usando GCC 8 y -std=c++17 entonces necesitarías recompilar F con la misma versión (o no vincular a F) porque los símbolos 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 el libstdc++.so biblioteca compartida de GCC 7 (o posterior). Debido a que el objeto F se compiló con GCC 7, necesita usar la biblioteca compartida de esa versión, porque compilar cualquier parte del programa con GCC 7 podría introducir dependencias en símbolos que no están presentes en el libstdc++.sode GCC 4.9 o GCC 5. De manera similar, si enlazaba al objeto G, construido con GCC 8, necesitaría usar el libstdc++.so de GCC 8 para asegurar que se encuentren todos los símbolos que G necesita. La regla simple es garantizar que la biblioteca compartida que el programa utiliza en tiempo de ejecución sea al menos tan nueva como la versión utilizada para compilar cualquiera de los objetos.

Otra advertencia al utilizar GCC, ya mencionada en los comentarios sobre su pregunta, es que desde GCC 5 hay dos implementaciones de std::string disponible en libstdc ++. Las dos implementaciones no son compatibles con el enlace (tienen diferentes nombres destrozados, por lo que no pueden vincularse entre sí), pero pueden coexistir en el mismo binario (tienen diferentes nombres destrozados, por lo que no entran en conflicto si un objeto utiliza std::string y los otros usos std::__cxx11::string) Si tus objetos usan std::string por lo general, todos deberían compilarse con la misma implementación de cadenas. Compilar con -D_GLIBCXX_USE_CXX11_ABI=0 para seleccionar el original gcc4-compatible implementación, o -D_GLIBCXX_USE_CXX11_ABI=1 para seleccionar el nuevo cxx11 implementación (no se deje engañar por el nombre, se puede usar también en C ++ 03, se llama cxx11 porque cumple con los requisitos de C ++ 11). Qué implementación es la predeterminada depende de cómo se configuró GCC, pero el valor predeterminado siempre puede anularse en tiempo de compilación con la macro.


27
2018-03-05 21:38



Hay dos partes en la respuesta. Compatibilidad en el nivel de compilador y compatibilidad en el nivel del enlazador. Comencemos con el primero.

supongamos que todos los encabezados fueron escritos en C ++ 11

Usar el mismo compilador significa que se usará el mismo encabezado de biblioteca estándar y los archivos de origen (los onces asociados con el compilador) independientemente del estándar de 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 particular de C ++, entonces las características que solo están disponibles en estándares más nuevos no deberían ser accesibles. Esto se hace usando el __cplusplus directiva. Ver el vector archivo fuente para un ejemplo interesante de cómo se usa. De forma similar, 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 que se dirigen a diferentes estándares de C ++. Esto se discute en el Anexo C del estándar C ++. Hay 4 cláusulas, solo discutiré la primera, y menciono brevemente el resto.

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

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

#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 el objetivo es C ++ 11 y C ++ 14, respectivamente. Cuando se dirige a C ++ 11, la coma dentro de las comillas no se considera un separador de parámetros; solo hay un solo parámetro Por lo tanto, el código sería equivalente a:

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

Por otro lado, cuando se apunta a C ++ 14, las comillas simples se interpretan como separadores de dígitos. Por lo 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 cabecera puros de C ++ 11 puede provocar errores sorprendentes en las unidades de traducción que se dirigen a C ++ 14/17. Por lo tanto, incluso si un archivo de cabecera está escrito en C ++ 11, debe escribirse cuidadosamente para garantizar que sea compatible con las versiones posteriores del estándar. los __cplusplus directiva puede ser ú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)

Razón fundamental: Se requiere para la 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 operador   la eliminación puede coincidir con un operador predefinido habitual (sin ubicación)   (3.7.4). Si es así, el programa está mal formado, como lo fue para el miembro de la clase   funciones de asignación y funciones de desasignación (5.3.4).

C.3.3 Cláusula 7: declaraciones

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

Razón fundamental: Es necesario permitir que las funciones de los constenedores muten el   objeto.

Efecto en la característica original: Es posible que el código válido de C ++ 2011 no compile en este   Estándar internacional.

Por ejemplo, el siguiente código es válido en C ++ 2011 pero no válido en   este estándar internacional porque declara el mismo miembro   funcionar dos veces con diferentes tipos de devolución:

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

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

Cambio: get no está definido.

Razón fundamental: El uso de get se considera peligroso.

Efecto en la característica original: Código válido de C ++ 2011 que usa el get   la función puede no compilarse en esta norma internacional.

Las incompatibilidades potenciales 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 se producirán, por lo que no los mencionaré aquí.

Ahora discutiré la compatibilidad en el nivel del enlazador. En general, los posibles motivos de incompatibilidad incluyen los siguientes:

Si el formato del archivo de objeto resultante depende del estándar de C ++ objetivo, 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 compilador en sí. De hecho, ninguno de los enlazadores de GCC, LLVM y VC ++ requieren conocimiento sobre el estándar de C ++ objetivo. 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 de C ++ y las diferentes rutinas no son compatibles entre sí, entonces no sería posible vincular los archivos del objeto. En GCC, LLVM y VC ++, afortunadamente este no es el caso. Además, la firma del main La función (y las restricciones que se aplican a ella, consulte la Sección 3.6 del estándar) es la misma en todos los estándares de C ++, por lo que no importa en qué unidad de traducción exista.

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

Por lo tanto, GCC, LLVM y VC ++ se han diseñado para permitir binario compatibilidad en diferentes versiones del estándar C ++. Sin embargo, este no es un requisito del estándar en sí mismo.

Por cierto, aunque el compilador de VC ++ ofrece el interruptor estándar, que le permite dirigirse a una versión particular del estándar C ++, no admite la orientación a C ++ 11. La versión mínima que se puede especificar es C ++ 14, que es el valor predeterminado a partir de la actualización 3 de Visual C ++ 2013. Puede utilizar 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 se dirigen a diferentes versiones del estándar C ++, que al menos romper WPO.

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


8
2018-03-05 20:26



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

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

Pero biblioteca estándar ...

Cada versión del compilador viene con una implementación de la biblioteca estándar de 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. También en algunos casos puede usar otra implementación de biblioteca estándar que el compilador proporcionado. Lo que debe importar es vincular una implementación de biblioteca estándar anterior 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.


2
2018-03-05 10:15