Atteindre la compatibilité aval avec C ++ 11

12

Je travaille sur une grande application logicielle qui doit fonctionner sur plusieurs plateformes. Certaines de ces plates-formes prennent en charge certaines fonctionnalités de C ++ 11 (par exemple MSVS 2010) et certaines n'en prennent pas en charge (par exemple GCC 4.3.x). Je m'attends à ce que cette situation se poursuive pendant plusieurs années (ma meilleure estimation: 3-5 ans).

Compte tenu de cela, je voudrais configurer une interface de compatibilité telle que (dans la mesure du possible) les gens puissent écrire du code C ++ 11 qui sera toujours compilé avec des compilateurs plus anciens avec un minimum de maintenance. Dans l'ensemble, l'objectif est de minimiser autant que raisonnablement possible # ifdef tout en activant la syntaxe / les fonctionnalités de base de C ++ 11 sur les plates-formes qui les prennent en charge, et de fournir une émulation sur les plates-formes qui ne le font pas.

Commençons par std :: move (). La façon la plus évidente d'atteindre la compatibilité serait de mettre quelque chose comme ça dans un fichier d'en-tête commun:

#if !defined(HAS_STD_MOVE)
namespace std { // C++11 emulation
  template <typename T> inline T& move(T& v) { return v; }
  template <typename T> inline const T& move(const T& v) { return v; }
}
#endif // !defined(HAS_STD_MOVE)

Cela permet aux gens d'écrire des choses comme

std::vector<Thing> x = std::move(y);

... en toute impunité. Il fait ce qu'il veut en C ++ 11 et fait de son mieux en C ++ 03. Lorsque nous supprimons enfin le dernier des compilateurs C ++ 03, ce code peut rester tel quel.

Cependant, selon la norme, il est illégal d'injecter de nouveaux symboles dans l' stdespace de noms. Voilà la théorie. Ma question est la suivante: en pratique, y a-t-il un inconvénient à le faire pour parvenir à une compatibilité aval?

mcmcc
la source
1
Boost fournit déjà une bonne partie de cela, et a déjà du code pour utiliser de nouvelles fonctionnalités quand / où elles sont disponibles, vous pourriez donc être en mesure d'utiliser simplement ce que Boost fournit et d'en finir. Bien sûr, il y a des limites - la plupart des nouvelles fonctionnalités ont été ajoutées spécifiquement parce que les solutions basées sur la bibliothèque ne sont pas adéquates.
Jerry Coffin du
oui, je pense spécifiquement aux fonctionnalités qui peuvent être implémentées au niveau de la bibliothèque, pas aux changements syntaxiques. Boost ne résout pas vraiment le problème de la compatibilité ascendante (transparente). À moins que je manque quelque chose ...
mcmcc
Gcc 4.3 a déjà une bonne poignée de fonctionnalités C ++ 11, les références Rvalue étant probablement les plus importantes.
Jan Hudec
@JanHudec: Vous avez raison. Pauvre exemple. Dans tous les cas, il existe d'autres compilateurs qui ne prennent certainement pas en charge la syntaxe (par exemple, quelle que soit la version du compilateur C ++ d'IBM que nous avons).
mcmcc

Réponses:

9

Je travaille depuis un bon moment à maintenir un niveau de compatibilité ascendante et descendante dans mes programmes C ++, jusqu'à ce que je devais finalement en faire une boîte à outils de bibliothèque , que je prépare pour la sortie . En général, tant que vous acceptez que vous n'obtiendrez pas de compatibilité ascendante "parfaite" ni dans les fonctionnalités (certaines choses ne peuvent tout simplement pas être émulées vers l'avant) ni dans la syntaxe (vous devrez probablement utiliser des macros, des espaces de noms alternatifs pour certaines choses) alors vous êtes prêt.

De nombreuses fonctionnalités peuvent être émulées en C ++ 03 à un niveau suffisant pour une utilisation pratique - et sans tous les tracas qui accompagnent, par exemple: Boost. Heck, même la proposition de normes C ++ nullptrsuggère un backport C ++ 03. Et puis il y a TR1 par exemple pour tout ce qui est en C ++ 11, mais nous avons eu des aperçus pendant des années. Non seulement cela, certaines fonctionnalités C ++ 14 comme assert des variantes, des foncteurs transparents et optional peuvent être implémentées en C ++ 03!

Les deux seules choses que je sais qui ne peuvent absolument pas être rétroportées sont les modèles constexpr et variadic.

En ce qui concerne toute la question de l'ajout de choses à l'espace de noms std, je pense que cela n'a pas d'importance - du tout. Pensez à Boost, l'une des bibliothèques C ++ les plus importantes et pertinentes, et à leur implémentation de TR1: Boost.Tr1. Si vous voulez améliorer C ++, rendez-le compatible avec C ++ 11, puis par définition, vous le transformez en quelque chose qui n'est pas C ++ 03, donc vous bloquer sur une norme que vous avez l'intention d'éviter ou de laisser derrière vous est , tout simplement, contre-productif. Les puristes se plaindront, mais par définition, il ne faut pas s'en soucier.

Bien sûr, ce n'est pas parce que vous ne suivrez pas la norme (03) que vous ne pouvez pas essayer de le faire ou que vous allez joyeusement faire le tour de la briser. Ce n'est pas le propos. Tant que vous gardez un contrôle très attentif sur ce qui est ajouté à l' stdespace de noms et que vous contrôlez les environnements dans lesquels votre logiciel est utilisé (c'est-à-dire: faites des tests!), Il ne devrait y avoir aucun dommage intraitable. Si possible, définissez tout dans un espace de noms séparé et ajoutez uniquement des usingdirectives à l'espace de noms stdafin de ne rien y ajouter au-delà de ce qui doit "absolument" entrer. Ce qui, IINM, est plus ou moins ce que fait Boost.TR1.


Mise à jour (2013) : comme la demande de la question d'origine et en voyant certains des commentaires que je ne peux pas ajouter en raison du manque de rep, voici une liste des fonctionnalités C ++ 11 et C ++ 14 et leur degré de portabilité en C ++ 03:

  • nullptr: pleinement réalisable compte tenu du backport officiel du Comité; vous devrez probablement également fournir certaines spécialisations type_traits afin qu'il soit reconnu comme un type "natif".
  • forward_list: entièrement implémentable, bien que le support d'allocateur repose sur ce que votre implication Tr1 peut fournir.
  • Nouveaux algorithmes (partition_copy, etc.): entièrement implémentables.
  • Constructions de conteneurs à partir de séquences d'accolades (par exemple :) vector<int> v = {1, 2, 3, 4};: entièrement implémentables, bien que plus verbeuses que ce que l'on souhaiterait.
  • static_assert: presque entièrement implémentable lorsqu'il est implémenté en tant que macro (vous n'aurez qu'à faire attention aux virgules).
  • unique_ptr: presque entièrement implémentable, mais vous aurez également besoin de l'aide du code appelant (pour les stocker dans des conteneurs, etc.); voir cependant ci-dessous.
  • rvalue-references: presque entièrement implémentable en fonction de ce que vous attendez d'eux (par exemple: Boost Move).
  • Pour chaque itération: presque entièrement implémentable, la syntaxe diffère quelque peu.
  • utiliser des fonctions locales comme arguments (par exemple: transformer): presque entièrement implémentable, mais la syntaxe sera suffisamment différente - par exemple, les fonctions locales ne sont pas définies sur le site de l'appel mais juste avant.
  • opérateurs de conversion explicites: implémentables à des niveaux pratiques (obtenir la conversion explicite), voir "explicit_cast" d' Imperfect C ++ ; mais l'intégration avec des fonctionnalités linguistiques telles que static_cast<>pourrait être presque impossible.
  • transfert d'argument: implémentable à des niveaux pratiques compte tenu de ce qui précède sur rvalue-references, mais vous devrez fournir N surcharges à vos fonctions en prenant des arguments transmissibles.
  • move: implémentable à des niveaux pratiques (voir les deux ci-dessus). Bien sûr, vous devez utiliser des conteneurs et des objets de modification pour en profiter.
  • Allocateurs de portée: pas vraiment implémentables à moins que votre implémentation Tr1 puisse les aider.
  • types de caractères multi-octets: Pas vraiment implémentable à moins que votre Tr1 ne puisse vous supporter. Mais dans le but prévu, il est préférable de s'appuyer sur une bibliothèque spécialement conçue pour traiter le problème, comme ICU, même si vous utilisez C ++ 11.
  • Listes d'arguments variadiques: implémentables avec quelques tracas, faites attention au transfert d'arguments.
  • noexcept: dépend des fonctionnalités de votre compilateur.
  • De nouvelles autosémantique et decltype: dépend des caractéristiques de votre compilateur - par exemple .: __typeof__.
  • types entiers de taille ( int16_t, etc.): dépend des fonctionnalités de votre compilateur - ou vous pouvez déléguer à stdint.h portable.
  • Attributs de type: dépend des fonctionnalités de votre compilateur.
  • Liste d'initialisation: non réalisable à ma connaissance; cependant, si vous voulez initialiser des conteneurs avec des séquences, voir ci-dessus sur les "constructions de conteneurs".
  • Alias ​​de modèle: pas implémentable à ma connaissance, mais c'est une fonctionnalité inutile de toute façon, et nous avons toujours eu des ::typemodèles
  • Modèles variadiques: non réalisables à ma connaissance; la fermeture est un argument de modèle par défaut, qui nécessite N spécialisations, etc.
  • constexpr: Non réalisable à ma connaissance.
  • Initialisation uniforme: Non implémentable à ma connaissance, mais l' initialisation par défaut du constructeur garantie peut être implémentée comme valeur initialisée de Boost.
  • C ++ 14 dynarray: entièrement implémentable.
  • C ++ 14 optional<>: presque entièrement implémentable tant que votre compilateur C ++ 03 prend en charge les configurations d'alignement.
  • Foncteurs transparents C ++ 14: presque entièrement implémentables, mais votre code client devra probablement utiliser explicitement par exemple: std::less<void>pour le faire fonctionner.
  • C ++ 14 nouvelles variantes d'assertion (telles que assure): entièrement implémentables si vous voulez des assertions, presque entièrement implémentables si vous souhaitez activer les lancers à la place.
  • Extensions de tuple C ++ 14 (obtenir l'élément tuple par type): entièrement implémentable, et vous pouvez même le faire échouer à compiler avec les cas exacts décrits dans la proposition de fonctionnalité.

(Avertissement: plusieurs de ces fonctionnalités sont implémentées dans ma bibliothèque de rétroportages C ++ que j'ai liée ci-dessus, donc je pense que je sais de quoi je parle quand je dis "complètement" ou "presque complètement".)

Luis Machuca
la source
6

C'est fondamentalement impossible. Considérez std::unique_ptr<Thing>. S'il était possible d'émuler des références rvalue en tant que bibliothèque, ce ne serait pas une fonction de langage.

DeadMG
la source
1
J'ai dit "dans la mesure du possible". De toute évidence, certaines fonctionnalités devront être laissées derrière # ifdef ou ne pas être utilisées du tout. std :: move () se trouve être celui que vous pouvez prendre en charge la syntaxe (mais pas la fonctionnalité).
mcmcc
2
En fait, la proposition de références rvalue mentionne une solution basée sur la bibliothèque!
Jan Hudec
Plus précisément, il est possible d'implémenter la sémantique de déplacement pour une classe particulière en C ++ 03, il devrait donc être possible de la définir std::unique_ptr, mais certaines autres fonctionnalités des références rvalue ne peuvent pas être implémentées en C ++ 03, ce std::forwardn'est donc pas possible. L'autre chose est que le std::unique_ptrne sera pas utile, car les collections n'utiliseront pas la sémantique de déplacement à moins que vous ne les remplaciez toutes.
Jan Hudec
@JanHudec: Il n'est pas possible de définir unique_ptr. Regardez les défauts de auto_ptr. unique_ptrest pratiquement l' exemple de manuel d'une classe dont la sémantique était fondamentalement activée par la fonction de langage.
DeadMG
@DeadMG: Non, ce n'est pas ce unique_ptrqui était fondamentalement activé par la fonction de langue. Il ne serait cependant pas très utile sans cette fonctionnalité. car sans transfert parfait, il ne serait pas utilisable dans de nombreux cas et un transfert parfait nécessite cette fonctionnalité.
Jan Hudec
2
  1. Gcc a commencé à introduire C ++ 11 (toujours C ++ 0x à l'époque) en 4.3. Ce tableau indique qu'il a déjà des références rvalue et quelques autres fonctionnalités moins utilisées (vous devez spécifier une -std=c++0xoption pour les activer).
  2. De nombreux ajouts à la bibliothèque standard en C ++ 11 étaient déjà définis dans TR1 et GNU stdlibc ++ les fournit dans l'espace de noms std :: tr1. Il suffit donc d'effectuer une utilisation conditionnelle appropriée.
  3. Boost définit la plupart des fonctions TR1 et peut les injecter dans l'espace de noms TR1 si vous ne l'avez pas (mais VS2010 le fait et gcc 4.3 le fait aussi si vous utilisez GNU stdlibc ++).
  4. Mettre quoi que ce soit dans l' stdespace de noms est un "comportement indéfini". Cela signifie que la spécification ne dit pas ce qui se passera. Mais si vous savez que sur une plate-forme particulière, la bibliothèque standard ne définit pas quelque chose, allez-y et définissez-le. Attendez-vous simplement à devoir vérifier sur chaque plateforme ce dont vous avez besoin et ce que vous pouvez définir.
  5. La proposition de références de valeur, N1690 mentionne comment implémenter la sémantique de déplacement en C ++ 03. Cela pourrait être utilisé pour remplacer unique_ptr. Cependant, cela ne serait pas trop utile, car il repose sur des collections utilisant réellement la sémantique de déplacement et celles de C ++ 03 ne le seront évidemment pas.
Jan Hudec
la source
1
Vous avez raison sur GCC, mais malheureusement, je dois également prendre en charge d'autres compilateurs (non-GCC). Votre puce n ° 4 est au cœur de la question que je pose. # 5 est intéressant mais je ne cherche pas à supporter la sémantique des mouvements (l'optimisation de la copie) sur ces anciennes plates-formes mais plutôt juste "std :: move ()" comme syntaxe compilable.
mcmcc