77 votes

Est-il sûr de lier des objets C++17, C++14 et C++11 ?

Supposons que j'aie trois objets compilés, tous produits par la fonction même compilateur/version :

  1. A a été compilé avec la norme C++11
  2. B a été compilé avec la norme C++14
  3. C a été compilé avec la norme C++17

Pour simplifier, supposons que tous les en-têtes ont été écrits en C++11, en utilisant uniquement des constructions dont la sémantique n'a pas changé entre les trois versions de la norme Ainsi, toute interdépendance a été correctement exprimée par l'inclusion de l'en-tête et le compilateur n'a pas émis d'objection.

Quelles combinaisons de ces objets est-il et n'est-il pas sûr de relier dans un seul binaire ? Pourquoi ?


EDIT : les réponses couvrant les principaux compilateurs (par exemple, gcc, clang, vs++) sont les bienvenues.

95voto

Jonathan Wakely Points 45593

Quelles combinaisons de ces objets est-il et n'est-il pas sûr de relier dans un seul binaire ? Pourquoi ?

Pour le CCG il est possible de relier en toute sécurité n'importe quelle combinaison d'objets A, B et C. S'ils sont tous construits avec la même version, ils sont compatibles ABI, la version standard (c'est-à-dire la version -std ) ne fait aucune différence.

Pourquoi ? Parce que c'est une propriété importante de notre mise en œuvre, que nous nous efforçons de garantir.

Là où vous avez des problèmes, c'est si vous liez ensemble des objets compilés avec différentes versions de GCC. et vous avez utilisé des fonctionnalités instables d'un nouveau standard C++ avant que le support de GCC pour ce standard ne soit complet. Par exemple, si vous compilez un objet en utilisant GCC 4.9 et -std=c++11 et un autre objet avec GCC 5 et -std=c++11 vous aurez des problèmes. Le support C++11 était expérimental dans GCC 4.x, et il y a donc eu des changements incompatibles entre les versions GCC 4.9 et 5 des fonctionnalités C++11. De même, si vous compilez un objet avec GCC 7 et -std=c++17 et un autre objet avec GCC 8 et -std=c++17 vous aurez des problèmes, car la prise en charge de C++17 dans GCC 7 et 8 est encore expérimentale et évolutive.

En revanche, n'importe quelle combinaison des objets suivants fonctionnera (voir cependant la remarque ci-dessous concernant libstdc++.so version) :

  • objet D compilé avec GCC 4.9 et -std=c++03
  • E compilé avec GCC 5 et -std=c++11
  • objet F compilé avec GCC 7 et -std=c++17

En effet, le support C++03 est stable dans les trois versions de compilateurs utilisées, et les composants C++03 sont donc compatibles entre tous les objets. Le support C++11 est stable depuis GCC 5, mais l'objet D n'utilise aucune fonctionnalité C++11, et les objets E et F utilisent tous deux des versions où le support C++11 est stable. Le support C++17 n'est stable dans aucune des versions de compilateur utilisées, mais seul l'objet F utilise des fonctionnalités C++17 et il n'y a donc aucun problème de compatibilité avec les deux autres objets (les seules fonctionnalités qu'ils partagent proviennent de C++03 ou C++11, et les versions utilisées rendent ces parties OK). Si vous vouliez plus tard compiler un quatrième objet, G, en utilisant GCC 8 et -std=c++17 alors vous devrez recompiler F avec la même version (ou ne pas faire de lien vers F) car les symboles C++17 de F et G sont incompatibles.

La seule réserve pour la compatibilité décrite ci-dessus entre D, E et F est que votre programme doit utiliser l'attribut libstdc++.so de la bibliothèque partagée de GCC 7 (ou plus). Parce que l'objet F a été compilé avec GCC 7, vous devez utiliser la bibliothèque partagée de cette version, car la compilation de n'importe quelle partie du programme avec GCC 7 pourrait introduire des dépendances sur des symboles qui ne sont pas présents dans la bibliothèque partagée de la version 7. libstdc++.so à partir de GCC 4.9 ou GCC 5. De la même manière, si vous créez un lien vers l'objet G, construit avec GCC 8, vous devrez utiliser la commande libstdc++.so de GCC 8 pour s'assurer que tous les symboles nécessaires à G sont trouvés. La règle simple consiste à s'assurer que la bibliothèque partagée que le programme utilise au moment de l'exécution est au moins aussi récente que la version utilisée pour compiler l'un des objets.

Une autre mise en garde concernant l'utilisation de GCC, déjà mentionnée dans les commentaires de votre question, est que depuis GCC 5 il y a deux mises en œuvre de std::string disponible dans libstdc++. Les deux implémentations ne sont pas compatibles entre elles (elles ont des noms mangés différents, et ne peuvent donc pas être liées entre elles) mais peuvent coexister dans le même binaire (elles ont des noms mangés différents, et n'entrent donc pas en conflit si un objet utilise la fonction std::string et les autres utilisations std::__cxx11::string ). Si vos objets utilisent std::string alors, en général, ils devraient tous être compilés avec la même implémentation de chaîne. Compiler avec -D_GLIBCXX_USE_CXX11_ABI=0 pour sélectionner l'original gcc4-compatible la mise en œuvre, ou -D_GLIBCXX_USE_CXX11_ABI=1 pour sélectionner le nouveau cxx11 (ne vous laissez pas tromper par le nom, elle peut aussi être utilisée en C++03, elle s'appelle cxx11 car il est conforme aux exigences de C++11). L'implémentation par défaut dépend de la configuration de GCC, mais elle peut toujours être remplacée à la compilation par la macro.

14voto

Hadi Brais Points 7944

Il y a deux parties à la réponse. La compatibilité au niveau du compilateur et la compatibilité au niveau de l'éditeur de liens. Commençons par la première.

supposons que tous les en-têtes aient été écrits en C++11

L'utilisation du même compilateur signifie que les mêmes fichiers d'en-tête et sources de la bibliothèque standard (les onces associées au compilateur) seront utilisés quelle que soit la norme C++ cible. Par conséquent, les fichiers d'en-tête de la bibliothèque standard sont écrits pour être compatibles avec toutes les versions de C++ supportées par le compilateur.

Cela dit, si les options du compilateur utilisées pour compiler une unité de traduction spécifient une norme C++ particulière, les fonctionnalités qui ne sont disponibles que dans les normes plus récentes ne doivent pas être accessibles. Ceci est fait en utilisant l'option __cplusplus directive. Voir la vecteur pour un exemple intéressant de son utilisation. De même, le compilateur rejettera toute caractéristique syntaxique offerte par des versions plus récentes de la norme.

Tout cela signifie que votre hypothèse ne peut s'appliquer qu'aux fichiers d'en-tête que vous avez écrits. Ces fichiers d'en-tête peuvent provoquer des incompatibilités lorsqu'ils sont inclus dans différentes unités de traduction visant des normes C++ différentes. Ce point est abordé dans l'annexe C de la norme C++. Il y a 4 clauses, je ne discuterai que de la première, et mentionnerai brièvement les autres.

C.3.1 Clause 2 : conventions lexicales

Les guillemets simples délimitent un littéral de caractère en C++11, alors qu'ils sont des séparateurs de chiffres en C++14 et C++17. Supposons que vous ayez la définition de macro suivante dans l'un des fichiers d'en-tête C++11 purs :

#define M(x, ...) __VA_ARGS__

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

Considérons deux unités de traduction qui incluent le fichier d'en-tête, mais qui ciblent respectivement C++11 et C++14. En ciblant C++11, la virgule entre les guillemets n'est pas considérée comme un séparateur de paramètres ; il n'y a qu'un seul paramètre. Par conséquent, le code serait équivalent à :

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

Par contre, en ciblant C++14, les guillemets simples sont interprétés comme des séparateurs de chiffres. Par conséquent, le code serait équivalent à :

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

Le point ici est que l'utilisation de guillemets simples dans l'un des fichiers d'en-tête C++11 purs peut entraîner des bogues surprenants dans les unités de traduction qui ciblent C++14/17. Par conséquent, même si un fichier d'en-tête est écrit en C++11, il doit être écrit avec soin pour s'assurer qu'il est compatible avec les versions ultérieures de la norme. Le site __cplusplus peut être utile ici.

Les trois autres clauses de la norme sont les suivantes :

C.3.2 Clause 3 : concepts de base

Changement : Nouveau désalloueur habituel (non-placement)

Justification : Requis pour la désallocation de taille.

Effet sur la caractéristique originale : Un code C++2011 valide pourrait déclarer une fonction d'allocation de placement global et une fonction de désallocation comme suit :

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

Dans la présente Norme internationale, cependant, la déclaration de l'opérateur delete peut correspondre à un opérateur delete habituel prédéfini (non-placement) (3.7.4). Si c'est le cas, le programme est mal formé, comme c'était le cas pour les fonctions d'allocation de membres de classe et les fonctions de transaction. et les fonctions de désallocation (5.3.4).

C.3.3 Clause 7 : déclarations

Changement Les fonctions membres non statiques constexpr ne sont pas des fonctions membres implicitement constantes. des fonctions membres.

Justification : Nécessaire pour permettre aux fonctions membres constexpr de muter l'objet. l'objet.

Effet sur la caractéristique originale : Le code C++2011 valide peut échouer à compiler dans ce norme internationale.

Par exemple, le code suivant est valide en C++2011 mais invalide en cette Norme Internationale car il déclare le même membre deux fois avec des types de retour différents :

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

C.3.4 Clause 27 : bibliothèque d'entrées/sorties

Changement : n'est pas défini.

Justification : L'utilisation du gets est considérée comme dangereuse.

Effet sur la caractéristique originale : Le code C++2011 valide qui utilise la fonction gets peut échouer à compiler dans cette norme internationale.

Les incompatibilités potentielles entre C++14 et C++17 sont discutées en C.4. Puisque tous les fichiers d'en-tête non standard sont écrits en C++11 (comme spécifié dans la question), ces problèmes ne se produiront pas, donc je ne les mentionnerai pas ici.

Maintenant, je vais discuter de la compatibilité au niveau du linker. En général, les raisons potentielles des incompatibilités sont les suivantes :

Si le format du fichier objet résultant dépend du standard C++ cible, l'éditeur de liens doit être capable de lier les différents fichiers objets. Dans GCC, LLVM et VC++, ce n'est heureusement pas le cas. C'est-à-dire que le format des fichiers objets est le même quel que soit le standard cible, bien qu'il dépende fortement du compilateur lui-même. En fait, aucun des linkers de GCC, LLVM et VC++ ne nécessite de connaître la norme C++ cible. Cela signifie également que nous pouvons lier des fichiers d'objets qui sont déjà compilés (liaison statique du runtime).

Si la routine de démarrage du programme (la fonction qui appelle main ) est différente pour les différents standards C++ et les différentes routines ne sont pas compatibles entre elles, alors il ne serait pas possible de lier les fichiers objets. Dans GCC, LLVM et VC++, ce n'est heureusement pas le cas. De plus, la signature de l'objet main (et les restrictions qui s'y appliquent, voir la section 3.6 de la norme) est la même dans toutes les normes C++, donc peu importe dans quelle unité de traduction elle existe.

En général, la WPO peut ne pas fonctionner correctement avec des fichiers d'objets compilés selon des normes C++ différentes. Cela dépend exactement des étapes du compilateur qui nécessitent la connaissance de la norme cible et de celles qui ne le font pas, et de l'impact que cela a sur les optimisations interprocédurales qui traversent les fichiers objets. Heureusement, GCC, LLVM et VC++ sont bien conçus et n'ont pas ce problème (pas à ma connaissance).

C'est pourquoi GCC, LLVM et VC++ ont été conçus de manière à permettre binaire la compatibilité entre les différentes versions de la norme C++. Ce n'est cependant pas vraiment une exigence de la norme elle-même.

D'ailleurs, bien que le compilateur VC++ offre la fonction interrupteur std La version minimale qui peut être spécifiée est C++14, qui est la version par défaut à partir de Visual C++ 2013 Update 3. Vous pourriez utiliser une version plus ancienne de VC++ pour cibler C++11, mais vous devriez alors utiliser différents compilateurs VC++ pour compiler différentes unités de traduction qui ciblent différentes versions de la norme C++, ce qui casserait au minimum WPO.

AVERTISSEMENT : Ma réponse peut ne pas être complète ou très précise.

2voto

E. Vakili Points 394

Les nouvelles normes C++ se présentent en deux parties : les caractéristiques du langage et les composants de la bibliothèque standard.

Comme vous l'entendez par nouvelle norme Les changements dans le langage lui-même (par exemple, ranged-for) ne posent pratiquement aucun problème (il existe parfois des conflits dans les en-têtes des bibliothèques tierces avec les nouvelles fonctionnalités du langage standard).

Mais la bibliothèque standard...

Chaque version du compilateur est fournie avec une implémentation de la bibliothèque standard C++ (libstdc++ avec gcc, libc++ avec clang, bibliothèque standard MS C++ avec VC++,...) et exactement une implémentation, pas plusieurs implémentations pour chaque version standard. De plus, dans certains cas, vous pouvez utiliser une autre implémentation de la bibliothèque standard que celle fournie par le compilateur. Ce dont vous devez vous soucier, c'est de lier une ancienne implémentation de la bibliothèque standard avec une plus récente.

Le conflit qui pourrait se produire entre les bibliothèques tierces et votre code est la bibliothèque standard (et d'autres bibliothèques) qui est liée à ces bibliothèques tierces.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X