
Résumé des parties précédentes
En raison de restrictions sur la capacité à utiliser les compilateurs C ++ 11 et du manque d'alternativité, boost a voulu écrire sa propre implémentation de la bibliothèque C ++ 11 standard en plus de la bibliothèque C ++ 98 / C ++ 03 fournie avec le compilateur.
En plus des fichiers d'en-tête standard
type_traits ,
thread ,
mutex ,
chrono ,
nullptr.h a été ajouté
qui implémente
std :: nullptr_t et
core.h où des macros liées aux
fonctionnalités dépendantes du compilateur ont été ajoutées, ainsi que l'extension de la bibliothèque standard.
Lien vers GitHub avec le résultat d'aujourd'hui pour les impatients et les non-lecteurs:
Les engagements et les critiques constructives sont les bienvenus
Table des matières
PrésentationChapitre 1. Viam supervadet vadensChapitre 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endifChapitre 3. Recherche de l'implémentation nullptr parfaiteChapitre 4. Magie des modèles C ++....
4.1 On commence petit....
4.2 À propos du nombre d'erreurs miraculeuses que le journal compile pour nous....
4.3 Pointeurs et tout-tout....
4.4 Quoi d'autre est nécessaire pour la bibliothèque de modèlesChapitre 5
...
Chapitre 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Une fois que tout le code a été peigné un peu et divisé par des en-têtes «standard» en un
espace de noms stdex séparé
, j'ai continué à remplir
type_traits ,
nullptr.h et le même
core.h , qui contenait des macros pour déterminer la version de la norme utilisée par le compilateur et la prendre en charge
Nullptr natif ,
char16_t ,
char32_t et
static_assert .
En théorie, tout est simple - selon la norme C ++
(clause 14.8), la macro
__cplusplus doit être définie par le compilateur et correspondre à la version de la norme prise en charge:
C++ pre-C++98: #define __cplusplus 1 C++98: #define __cplusplus 199711L C++98 + TR1: #define __cplusplus 199711L
en conséquence, le code pour déterminer la disponibilité du support est trivial:
#if (__cplusplus >= 201103L)

En fait, tout n'est pas si simple et maintenant des béquilles intéressantes avec un râteau commencent.
Premièrement, tous les compilateurs, ou plutôt aucun, n'implémentent pas la norme suivante de manière complète et immédiate. Par exemple, dans Visual Studio 2013,
constexpr a été absent
pendant très longtemps, alors qu'il a été affirmé qu'il prend en charge C ++ 11 - avec la mise en garde que l'implémentation n'est pas terminée. Autrement dit,
auto - s'il vous plaît,
static_assert - est tout aussi facile (même à partir de MS VS antérieurs), mais
constexpr ne l' est
pas . Deuxièmement, tous les compilateurs (et c'est encore plus surprenant) n'exposent pas correctement cette définition et la mettent à jour en temps opportun. Du coup, dans le même compilateur, Visual Studio
n'a pas changé la version de la définition de
__cplusplus à partir des toutes premières versions du compilateur, bien que la prise en charge complète de C ++ 11 soit annoncée depuis longtemps (ce qui n'est également pas vrai, pour lequel il existe des rayons de mécontentement distincts - dès que la conversation porte sur la fonctionnalité spécifique du «nouveau»). "11 développeurs standard disent immédiatement qu'il n'y a pas de préprocesseur C99, il n'y a pas d'autres" fonctionnalités "). Et la situation est aggravée par le fait que les compilateurs standard sont autorisés à définir cette définition sur une valeur différente des valeurs ci-dessus, s'ils ne sont pas entièrement conformes aux normes déclarées. Il serait logique de supposer, par exemple, un tel développement de définitions pour une macro donnée (avec l'introduction de nouvelles fonctionnalités, augmentez le nombre caché derrière cette définition):
standart C++98: #define __cplusplus 199711L
Mais en même temps, aucun des principaux compilateurs populaires n'est "usé" avec cette fonctionnalité.
À cause de tout cela (je n'ai pas peur de ce mot), maintenant pour chaque compilateur non standard, vous devez écrire vos propres vérifications spécifiques afin de savoir quelle norme C ++ et dans quelle mesure elle prend en charge. La bonne nouvelle est que nous devons en savoir plus sur quelques fonctions du compilateur pour fonctionner correctement. Tout d'abord, nous ajoutons maintenant la vérification de version pour Visual Studio via la macro
_MSC_VER , unique à ce compilateur. Étant donné que dans mon arsenal de compilateurs pris en charge, il existe également C ++ Borland Builder 6.0, dont les développeurs, à leur tour, étaient très désireux de maintenir la compatibilité avec Visual Studio (y compris ses «fonctionnalités» et bogues), alors il y a soudainement cette macro là aussi. Pour les compilateurs compatibles avec Clang, il existe une macro non standard
__has_feature (nom_fonction ) , qui vous permet de savoir si le compilateur prend en charge telle ou telle fonctionnalité. En conséquence, le code est gonflé à:
#ifndef __has_feature #define __has_feature(x) 0
Vous voulez atteindre plus de compilateurs? Nous ajoutons des vérifications pour Codegear C ++ Builder, qui est l'héritier de Borland (dans ses pires manifestations, mais plus à ce sujet plus tard):
#ifndef __has_feature #define __has_feature(x) 0
Il convient également de noter que, puisque Visual Studio prend déjà en charge
nullptr à partir de la version du compilateur
_MSC_VER 1600 , ainsi que les types
intégrés char16_t et
char32_t , nous devons gérer cela correctement. Quelques contrôles supplémentaires ajoutés:
#ifndef __has_feature #define __has_feature(x) 0
En même temps, nous vérifierons également la prise en charge de C ++ 98, car pour les compilateurs sans, il n'y aura pas de fichiers d'en-tête de la bibliothèque standard, et nous ne pouvons pas vérifier leur absence à l'aide du compilateur.
Option complète #ifndef __has_feature #define __has_feature(x) 0
Et maintenant, de volumineuses configurations de boost commencent à apparaître dans ma mémoire dans lesquelles beaucoup de développeurs assidus ont écrit toutes ces macros dépendantes du compilateur et ont fait une carte de ce qui est pris en charge et de ce qui ne l'est pas par un compilateur spécifique d'une version spécifique, dont je me sens personnellement mal à l'aise, Je veux ne plus jamais le regarder ni le toucher. Mais la bonne nouvelle est que vous pouvez vous arrêter là. Au moins, cela me suffit pour prendre en charge les compilateurs les plus populaires, mais si vous trouvez une inexactitude ou si vous souhaitez ajouter un autre compilateur, je ne serai que trop heureux d'accepter la demande de tirage.
Une grande réussite par rapport au boost, je pense qu'il était possible de conserver la propagation des macros dépendantes du compilateur dans le code, ce qui rend le code plus propre et plus facile à comprendre, et n'empile pas non plus des dizaines de fichiers de configuration pour chaque OS et pour chaque compilateur. Nous parlerons un peu plus tard des inconvénients de cette approche.
À ce stade, nous pouvons déjà commencer à connecter les fonctionnalités manquantes des 11 normes, et la première chose que nous introduisons est
static_assert .
static_assert
Nous définissons la structure
StaticAssertion , qui prendra une valeur booléenne comme paramètre de modèle - il y aura notre condition, si elle n'est pas remplie (l'expression est
fausse ), une erreur se produira lors de la compilation d'un modèle non spécialisé. Et une autre structure factice pour recevoir
sizeof ( StaticAssertion ) .
namespace stdex { namespace detail { template <bool> struct StaticAssertion; template <> struct StaticAssertion<true> { };
et encore plus de magie magique
#ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) #else
utilisation:
STATIC_ASSERT(sizeof(void*) == 4, non_x32_platform_is_unsupported);
Une différence importante entre mon implémentation et celle standard est qu'il n'y a pas de surcharge de ce mot-clé sans en informer l'utilisateur. Cela est dû au fait qu'en C ++, il est impossible de définir plusieurs définitions avec un nombre d'arguments différent mais un seul nom, et une implémentation sans message est beaucoup moins utile que l'option sélectionnée. Cette fonctionnalité conduit au fait qu'en gros STATIC_ASSERT dans mon implémentation est la version ajoutée déjà en C ++ 11.
Jetons un coup d'œil à ce qui s'est passé. À la suite des vérifications des versions de
__cplusplus et des macros de compilateur non standard, nous avons suffisamment d'informations sur la prise en charge C ++ 11 (et donc
static_assert ), exprimées par la
définition de _STDEX_NATIVE_CPP11_SUPPORT. Par conséquent, si cette macro est définie, nous pouvons simplement utiliser le
static_assert standard:
#ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message)
Veuillez noter que le deuxième paramètre de la macro STATIC_ASSERT n'est pas du tout un littéral de chaîne, et par conséquent, en utilisant l'opérateur de préprocesseur #, nous convertirons le paramètre de message en une chaîne pour la transmission au static_assert standard.
Si nous n'avons pas de support du compilateur, alors nous procédons à notre implémentation. Pour commencer, nous déclarerons des macros auxiliaires pour «coller» les chaînes (l'opérateur de préprocesseur
## en est juste responsable).
#define CONCATENATE(arg1, arg2) CONCATENATE1(arg1, arg2) #define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2) #define CONCATENATE2(arg1, arg2) arg1##arg2
En particulier, je n'ai pas utilisé simplement #define CONCATENATE ( arg1 , arg2 ) arg1 ## arg2 afin de pouvoir transmettre le résultat de la même macro CONCATENATE comme argument à arg1 et arg2 .
Ensuite, nous déclarons une structure avec le beau nom __static_assertion_at_line_ {numéro de ligne} (la macro
__LINE__ est également définie par la norme et doit être étendue au numéro de ligne sur lequel elle a été appelée), et à l'intérieur de cette structure, nous ajoutons un champ de notre type
StaticAssertion avec le nom STATIC_ASSERTION_FAILED_AT_LINE_ {numéro de ligne} _WITH __ messages d'erreur de la macro appelante}.
#define STATIC_ASSERT(expression, message)\ struct CONCATENATE(__static_assertion_at_line_, __LINE__)\ {\ stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\ };\ typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__)
Avec le paramètre de modèle dans
StaticAssertion, nous passons une expression qui est vérifiée dans
STATIC_ASSERT , la menant à
bool . Enfin, afin d'éviter de créer des variables locales et de vérifier la surcharge de l'utilisateur, un alias est déclaré pour le type
StaticAssertionTest <sizeof ({nom de la structure déclarée ci-dessus}) avec le nom __static_assertion_test_at_line_ {numéro de ligne}.
Toute la beauté de la dénomination est nécessaire uniquement pour indiquer clairement à partir d'une erreur de compilation qu'il s'agit d'un résultat d'assertion, et pas seulement d'une erreur, mais également pour afficher un message d'erreur qui a été défini pour cette assertion. L'astuce
sizeof est nécessaire pour forcer le compilateur à instancier la classe de modèle
StaticAssertion , qui se trouve à l'intérieur de la structure qui vient d'être déclarée, et ainsi vérifier la condition passée à assert.
Résultats STATIC_ASSERTGCC:
30: 103: erreur: le champ 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' a un type incomplet 'stdex :: detail :: StaticAssertion <false>'
25:36: note: dans la définition de la macro 'CONCATENATE2'
23:36: note: en expansion de la macro 'CONCATENATE1'
30:67: note: en expansion de la macro 'CONCATENER'
24:36: note: en expansion de la macro 'CONCATENATE2'
23:36: note: en expansion de la macro 'CONCATENATE1'
30:79: note: en expansion de la macro 'CONCATENER'
24:36: note: en expansion de la macro 'CONCATENATE2'
23:36: note: en expansion de la macro 'CONCATENATE1'
30:91: note: en expansion de la macro 'CONCATENER'
36: 3: note: en expansion de la macro 'STATIC_ASSERT'
Borland C ++ Builder:
[Erreur C ++] stdex_test.cpp (36): E2450 Structure non définie 'stdex :: detail :: StaticAssertion <0>'
[Erreur C ++] stdex_test.cpp (36): E2449 La taille de 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' est inconnue ou nulle
[Erreur C ++] stdex_test.cpp (36): E2450 Structure non définie 'stdex :: detail :: StaticAssertion <0>'
Visual Studio:
Erreur C2079
Le deuxième "truc" que je voulais avoir, tout en
manquant dans la norme, c'est
countof - compter le nombre d'éléments dans le tableau. Les Sishers aiment beaucoup déclarer cette macro via sizeof (arr) / sizeof (arr [0]), mais nous irons plus loin.
countof
#ifdef _STDEX_NATIVE_CPP11_SUPPORT #include <cstddef> namespace stdex { namespace detail { template <class T, std::size_t N> constexpr std::size_t _my_countof(T const (&)[N]) noexcept { return N; } } // namespace detail } #define countof(arr) stdex::detail::_my_countof(arr) #else //no C++11 support #ifdef _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT // Visual C++ fallback #include <stdlib.h> #define countof(arr) _countof(arr) #elif defined(_STDEX_NATIVE_CPP_98_SUPPORT)// C++ 98 trick #include <cstddef> template <typename T, std::size_t N> char(&COUNTOF_REQUIRES_ARRAY_ARGUMENT(T(&)[N]))[N]; #define countof(x) sizeof(COUNTOF_REQUIRES_ARRAY_ARGUMENT(x)) #else #define countof(arr) sizeof(arr) / sizeof(arr[0]) #endif
Pour les compilateurs
prenant en charge
constexpr ,
nous déclarerons une version constexpr de ce modèle (ce qui n'est absolument pas nécessaire, pour toutes les normes, l'implémentation via le modèle
COUNTOF_REQUIRES_ARRAY_ARGUMENT suffit ), pour le reste, nous introduisons la version via la fonction de modèle
COUNTOF_REQUIRES_ARRAY_ARGUMENT . Visual Studio se distingue à nouveau par la présence de sa propre implémentation de
_countof dans le fichier d'en-tête
stdlib.h .
La fonction
COUNTOF_REQUIRES_ARRAY_ARGUMENT semble intimidante et
comprendre ce qu'elle fait est assez délicat. Si vous regardez attentivement, vous pouvez comprendre qu'il prend un tableau d'éléments de type de modèle
T et de taille
N comme seul argument - ainsi, dans le cas du transfert d'autres types d'éléments (pas de tableaux), nous obtenons une erreur de compilation, ce qui est sans aucun doute agréable. En y regardant de plus près, vous pouvez comprendre (avec difficulté) qu'il retourne un tableau d'éléments
char de taille
N. La question est, pourquoi avons-nous besoin de tout cela? C'est là que l'opérateur
sizeof entre en
jeu et sa capacité unique à travailler au moment de la compilation. La taille de l'appel
( COUNTOF_REQUIRES_ARRAY_ARGUMENT ) détermine la taille du tableau d'éléments
char retourné par la fonction, et puisque la taille standard de
(char) == 1, c'est le nombre de
N éléments dans le tableau d'origine. Élégant, beau et totalement gratuit.
pour toujours
Une autre petite macro d'aide que j'utilise partout où une boucle infinie est nécessaire est
pour toujours . Il est défini comme suit:
#if !defined(forever) #define forever for(;;) #else #define STRINGIZE_HELPER(x) #x #define STRINGIZE(x) STRINGIZE_HELPER(x) #define WARNING(desc) message(__FILE__ "(" STRINGIZE(__LINE__) ") : warning: " desc) #pragma WARNING("stdex library - macro 'forever' was previously defined by user; ignoring stdex macro definition") #undef STRINGIZE_HELPER #undef STRINGIZE #undef WARNING #endif
Exemple de syntaxe pour définir une boucle infinie explicite:
unsigned int i = 0; forever { ++i; }
Cette macro est utilisée uniquement pour définir explicitement une boucle infinie et n'est incluse dans la bibliothèque que pour des raisons «d'ajout de sucre syntaxique». À l'avenir, je propose de le remplacer par éventuellement en définissant la macro du plug-in
FOREVER . Ce qui est remarquable dans l'extrait de code ci-dessus de la bibliothèque est la macro très
WARNING qui génère un message d'avertissement dans tous les compilateurs si la macro
forever a déjà été définie par l'utilisateur. Il utilise la macro
__LINE__ standard
familière et la
macro __FILE__ standard, qui est convertie en une chaîne avec le nom du fichier source actuel.
stdex_assert
Pour implémenter
assert en runtime, la macro
stdex_assert est introduite comme:
#if defined(assert) #ifndef NDEBUG #include <iostream> #define stdex_assert(condition, message) \ do { \ if (! (condition)) { \ std::cerr << "Assertion `" #condition "` failed in " << __FILE__ \ << " line " << __LINE__ << ": " << message << std::endl; \ std::terminate(); \ } \ } while (false) #else #define stdex_assert(condition, message) ((void)0) #endif #endif
Je ne dirai pas que je suis très fier de cette implémentation (elle sera modifiée à l'avenir), mais une technique intéressante a été utilisée ici sur laquelle je voudrais attirer l'attention. Afin de masquer les vérifications de la portée du code d'application, la construction
do {} while (false) est utilisée, qui sera exécutée, ce qui est évident une fois et en même temps n'introduira pas de code "service" dans le code d'application général. Cette technique est très utile et est utilisée à plusieurs autres endroits de la bibliothèque.
Sinon, l'implémentation est très similaire à l'
assertion standard - avec une certaine macro
NDEBUG , que les compilateurs définissent généralement dans les versions de version, assert ne fait rien, sinon elle interrompt l'exécution du programme avec la sortie du message dans le flux d'erreur standard si la condition assert n'est pas remplie.
sauf
Pour les fonctions qui ne
génèrent pas d'exceptions, le mot clé
noexcept a été introduit dans la nouvelle norme. Il est également assez simple et indolore à implémenter via la macro:
#ifdef _STDEX_NATIVE_CPP11_SUPPORT #define stdex_noexcept noexcept #else #define stdex_noexcept throw() #endif
cependant, il est nécessaire de comprendre que, selon la norme,
noexcept peut prendre la valeur
bool et
être également
utilisé pour déterminer au moment de la compilation que l'expression qui lui est passée ne
lève pas d'exception. Cette fonctionnalité ne peut pas être implémentée sans le support du compilateur, et il n'y a donc qu'un
stdex_noexcept «dépouillé» dans la bibliothèque.
La fin du deuxième chapitre. Le
troisième chapitre parlera des subtilités de l'implémentation nullptr, pourquoi elle est différente pour différents compilateurs, ainsi que du développement de type_traits, et quels autres bugs dans les compilateurs j'ai rencontrés au cours de son développement.
Merci de votre attention.