Comment ai-je écrit la bibliothèque C ++ 11 standard ou pourquoi boost est si effrayant. Chapitre 4.3

Nous continuons l'aventure.

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.

Static_assert , noexcept , countof ont été implémentés et, après avoir pris en compte toutes les définitions non standard et les fonctionnalités du compilateur, des informations sont apparues sur la fonctionnalité prise en charge par le compilateur actuel. Sa propre implémentation de nullptr est incluse , qui est sélectionnée au stade de la compilation.

Le temps est venu pour type_traits et tout ce "modèle magique spécial". Dans les parties précédentes de ce chapitre, nous avons examiné mon implémentation des modèles de base de la bibliothèque standard, et dans cette partie, nous parlerons de la combinaison de la technique SFINAE avec des modèles et un peu de la génération de code.

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
Plus de modèles C ++ sous cat.

Table des matières


Présentation
Chapitre 1. Viam supervadet vadens
Chapitre 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Chapitre 3. Recherche de l'implémentation nullptr parfaite
Chapitre 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èles
Chapitre 5
...

Chapitre 4. Modèle C ++ "magique". Continuation


4.3 Pointeurs et tout-tout


À ce stade, je ne pouvais obtenir que des informations sur le type de tableau pour std :: is_array et il était possible de démarrer des modèles de pointeurs. La mise en œuvre a également été triviale, mais non sans hypothèses.

// is_array template<class> struct is_array : public false_type { }; template<class _Tp, std::size_t _Size> struct is_array<_Tp[_Size]> : public true_type { }; /*template<class _Tp> struct is_array<_Tp[]>: public true_type { }; */ 

Une spécialisation de modèle simple pour les tableaux d'une longueur donnée «capture» tous les types de tableaux, cependant, le problème se pose avec le type incomplet T [] (un tableau sans spécifier la longueur). Le fait est que ce type n'est pas défini par certains compilateurs (C ++ Builder) lors de la spécialisation d'un modèle, et je n'ai pas encore trouvé de solution universelle ici.

Après avoir appris à la bibliothèque à définir les types intégrés, l'alignement dans la mémoire de type, à travailler avec des modificateurs de type et d'autres choses de base via des modèles au moment de la compilation, il était temps pour les pointeurs et les références.

image En C ++, deux groupes de pointeurs peuvent être distingués - des pointeurs vers des membres de classe et des pointeurs vers d'autres objets. Pourquoi cette séparation est-elle importante pour la poursuite de la mise en œuvre de la bibliothèque standard? Le fait est que les pointeurs vers les membres de la classe ont une différence significative par rapport à d'autres pointeurs par leur présence, c'est-à-dire pointeur vers un objet de cette classe. Et par défaut, les pointeurs vers un membre de classe ont une syntaxe distincte pour la définition, sont un type distinct et ne peuvent pas être représentés via un pointeur normal. En pratique, cela se traduit par le fait que la taille d'un pointeur vers un membre de la classe est généralement supérieure à la taille d'un pointeur normal (qui == sizeof (void *) ), car pour implémenter les fonctions de membre virtuel de la classe, ainsi que pour stocker le pointeur this , les compilateurs implémentent généralement des pointeurs vers un membre de classe en tant que structure (lisez les fonctions et la structure virtuelles). La manière de présenter des pointeurs aux membres de la classe est laissée, selon la norme, à la discrétion du compilateur, mais nous nous souviendrons de cette différence de taille et de présentation lors de l'examen de code supplémentaire.

Pour définir un pointeur régulier vers un objet, nous allons écrire un modèle is_pointer simple, ainsi qu'un modèle is_lvalue_reference pour les références d'objet ( nous mettons de côté is_rvalue_reference, car jusqu'au 11ème standard, il n'y avait pas d'opérateur && ainsi que la sémantique de mouvement complète):

 namespace detail { template<class> struct _is_pointer_helper : public false_type { }; template<class _Tp> struct _is_pointer_helper<_Tp*> : public true_type { }; } // is_pointer template<class _Tp> struct is_pointer : public detail::_is_pointer_helper<typename remove_cv<_Tp>::type>::type { }; // is_lvalue_reference template<class> struct is_lvalue_reference : public false_type { }; template<class _Tp> struct is_lvalue_reference<_Tp&> : public true_type { }; 

Il n'y a plus rien de fondamentalement nouveau ici, tout a été fait dans les parties précédentes de ce chapitre. Continuons à définir des pointeurs vers des objets - regardons maintenant les pointeurs vers les fonctions.
Il est important de comprendre qu'une fonction et une fonction membre d'une classe sont des entités complètement différentes selon la norme:

  • Le premier pointeur sera normal (un pointeur vers un objet), le second aura un pointeur vers un membre de la classe.

 void (*func_ptr)(int); //  'func_ptr'    'void func(int){}' void (ClassType::*mem_func_ptr)(int); //  'mem_func_ptr'  -  'ClassType'  'void ClassType::func(int){}' 

  • Vous pouvez créer un lien vers le premier (lien d'objet), mais vous ne pouvez pas créer un deuxième lien.

 void (&func_ref)(int); //  'func_ref'    'void func(int){}' //-------------------- //   -     
Ici, je vais juste parler un peu de la génération de code. Comme avant C ++ 11, il n'y avait pas de modèles avec un nombre variable de paramètres, tous les modèles où il pouvait y avoir un nombre différent de paramètres ont été déterminés par la spécialisation du modèle principal avec un grand nombre de paramètres à l'entrée et leur initialisation par défaut des paramètres factices. La même chose s'applique aux surcharges de fonction, Il n'y avait pas non plus de macros avec un nombre variable de paramètres. Depuis l'écriture de 60 à 70 lignes du même type de spécialisation de modèle avec vos mains, la surcharge des fonctions est une tâche plutôt ennuyeuse et inutile, et elle est également susceptible de faire une erreur. J'ai écrit un simple générateur de code pour les modèles et des surcharges de fonctions à ces fins. J'ai décidé de me limiter à définir des fonctions à 24 paramètres et cela semble plutôt lourd dans le code, mais simple et clair:

 namespace detail { template <class R> struct _is_function_ptr_helper : false_type {}; template <class R > struct _is_function_ptr_helper<R(*)()> : true_type {}; template <class R > struct _is_function_ptr_helper<R(*)(...)> : true_type {}; template <class R, class T0> struct _is_function_ptr_helper<R(*)(T0)> : true_type {}; template <class R, class T0> struct _is_function_ptr_helper<R(*)(T0 ...)> : true_type {}; 

...
  template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24> struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24)> : true_type {}; template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24> struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 ...)> : true_type {}; } 

Nous définissons les types qui nous sont familiers dans le chapitre précédent pour la technique SFINAE:

 namespace detail { // SFINAE magic typedef char _yes_type; struct _no_type { char padding[8]; }; } 

Quelques macros supplémentaires pour plus de commodité
 namespace detail { #define _IS_MEM_FUN_PTR_CLR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const volatile); #ifdef _STDEX_CDECL _no_type _STDEX_CDECL _is_mem_function_ptr(...); #define _IS_MEM_FUN_CDECL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const volatile); #define _IS_MEM_FUN_STDCALL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const volatile); #define _IS_MEM_FUN_FASTCALL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const volatile); #else _no_type _is_mem_function_ptr(...); #define _IS_MEM_FUN_CDECL_PTR #define _IS_MEM_FUN_STDCALL_PTR #define _IS_MEM_FUN_FASTCALL_PTR #endif #define _IS_MEM_FUN_PTR \ _IS_MEM_FUN_PTR_CLR \ _IS_MEM_FUN_CDECL_PTR \ _IS_MEM_FUN_STDCALL_PTR \ _IS_MEM_FUN_FASTCALL_PTR } 


Les macros sont définies de sorte qu'il est relativement pratique de redéfinir TYPES et ARGS définit comme une liste de types et de paramètres, puis en remplaçant la macro _IS_MEM_FUN_PTR pour générer des définitions pour tous les types de fonctions possibles par le préprocesseur. Il convient également de prêter attention au fait que pour les compilateurs de Microsoft, les accords d'appel ( __fastcall , __stdcall et __cdecl ) sont également importants , car avec des conventions différentes, les fonctions seront différentes, bien qu'elles aient le même ensemble d'arguments et la même valeur de retour. En conséquence, toute cette conception de macro grandiose est utilisée de manière assez compacte:

 namespace detail { #define TYPES #define ARGS _IS_MEM_FUN_PTR #undef TYPES #undef ARGS #define TYPES , class T0 #define ARGS T0 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS #define TYPES , class T0, class T1 #define ARGS T0, T1 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS 

...
  #define TYPES , class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24 #define ARGS T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS //      define  : #undef _IS_MEM_FUN_PTR #undef _IS_MEM_FUN_PTR_CLR #undef _IS_MEM_FUN_CDECL_PTR #undef _IS_MEM_FUN_STDCALL_PTR #undef _IS_MEM_FUN_FASTCALL_PTR } 

Et maintenant, pour ce que tout a été écrit:

 namespace detail { template <class _Tp, bool _IsRef> struct _is_mem_function_ptr_impl { static _Tp *p; static const bool value = (sizeof(_is_mem_function_ptr(_is_mem_function_ptr_impl::p)) == sizeof(_yes_type)); typedef typename integral_constant<bool, _is_mem_function_ptr_impl::value == bool(true)>::type type; }; template <class _Tp> struct _is_mem_function_ptr_impl<_Tp, true>: public false_type {}; template <class _Tp> struct _is_mem_function_ptr_helper: public _is_mem_function_ptr_impl<_Tp, is_reference<_Tp>::value>::type {}; template <class _Tp, bool _IsMemberFunctionPtr> struct _is_function_chooser_impl : public false_type { }; template <class _Tp> struct _is_function_chooser_impl<_Tp, false> : public _is_function_ptr_helper<_Tp*> { }; template<class _Tp, bool _IsRef = true> struct _is_function_chooser : public false_type { }; template <class _Tp> struct _is_function_chooser<_Tp, false> { static const bool value = _is_function_chooser_impl<_Tp, _is_mem_function_ptr_helper<_Tp>::value>::value; }; } 

Pour vérifier si un type est une fonction membre d'une classe, il est d'abord vérifié si le type est une référence. Ensuite, un pointeur de ce type est créé et substitué dans la fonction de sonde. En utilisant la technique SFINAE, le compilateur sélectionne la surcharge nécessaire des fonctions de sonde pour un tel pointeur et, en fonction du résultat de la comparaison avec _yes_type, forme le résultat.

Sur la base d'une vérification sur une fonction membre d'une classe, une vérification de type est écrite sur son appartenance au type de fonction. Nous vérifions si le type est référence, sinon, nous recherchons une spécialisation appropriée des structures de sonde de modèle pour un pointeur de ce type, qui sera true_type pour tous les pointeurs de fonction avec jusqu'à 24 paramètres.

Et maintenant, nous utilisons le résultat pour implémenter is_function . Ici, pour la même raison que dans la partie précédente , je n'ai pas pu hériter de cette structure de integr_constant , donc le comportement de integr_constant est "simulé".

 // is_function template<class _Tp> struct is_function { static const bool value = detail::_is_function_chooser<_Tp, is_reference<_Tp>::value>::value; typedef const bool value_type; typedef integral_constant<bool, is_function::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; 

Et pour implémenter is_member_function_pointer, c'est encore plus simple:

 // is_member_function_pointer template<class _Tp> struct is_member_function_pointer : public detail::_is_mem_function_ptr_helper<typename remove_cv<_Tp>::type>::type { }; 

De plus, sur la base de ces modèles, nous pouvons déterminer si le type est un membre de la classe en principe:

 namespace detail { template<class _Tp> struct _is_member_object_pointer_impl1 : public _not_< _or_<_is_function_ptr_helper<_Tp>, _is_mem_function_ptr_helper<_Tp> > >::type { }; template<class _Tp> struct _is_member_object_pointer_impl2 : public false_type { }; template<class _Tp, class _Cp> struct _is_member_object_pointer_impl2<_Tp _Cp::*> : public true_type { }; template<class _Tp> struct _is_member_object_pointer_helper: public _and_<_is_member_object_pointer_impl1<_Tp>, _is_member_object_pointer_impl2<_Tp> >::type {}; } // is_member_object_pointer template<class _Tp> struct is_member_object_pointer : public detail::_is_member_object_pointer_helper<typename remove_cv<_Tp>::type>::type { }; 

Opérations logiques 'et', 'ou', 'non' utilisées sur les types de la première partie
 namespace detail { struct void_type {}; //typedef void void_type; template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type> struct _or_ : public conditional<_B1::value, _B1, _or_<_B2, _or_<_B3, _B4> > >::type { }; template<> struct _or_<void_type, void_type, void_type, void_type>; template<class _B1> struct _or_<_B1, void_type, void_type, void_type> : public _B1 { }; template<class _B1, class _B2> struct _or_<_B1, _B2, void_type, void_type> : public conditional<_B1::value, _B1, _B2>::type { }; template<class _B1, class _B2, class _B3> struct _or_<_B1, _B2, _B3, void_type> : public conditional<_B1::value, _B1, _or_<_B2, _B3> >::type { }; template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type> struct _and_; template<> struct _and_<void_type, void_type, void_type, void_type>; template<class _B1> struct _and_<_B1, void_type, void_type, void_type> : public _B1 { }; template<class _B1, class _B2> struct _and_<_B1, _B2, void_type, void_type> : public conditional<_B1::value, _B2, _B1>::type { }; template<class _B1, class _B2, class _B3> struct _and_<_B1, _B2, _B3, void_type> : public conditional<_B1::value, _and_<_B2, _B3>, _B1>::type { }; template<class _Pp> struct _not_ { static const bool value = !bool(_Pp::value); typedef const bool value_type; typedef integral_constant<bool, _not_::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; } 


Ici, nous utilisons des opérations logiques sur les types qui, à l'aide du modèle conditionnel , sélectionnent éventuellement le type de modèle approprié. La programmation de modèles dans toute sa splendeur, par conséquent, au stade de la compilation, nous avons déjà des informations pour savoir si le type est membre de la classe. Assez "furieux", mais comme c'est spectaculaire et efficace!

Un peu plus de programmation de modèles purs sur les mêmes éléments logiques et nous avons is_fundamental , is_compound , etc. signes (cela me ravit, mais vous?):

 // is_arithmetic template<class _Tp> struct is_arithmetic : public detail::_or_<is_integral<_Tp>, is_floating_point<_Tp> >::type { }; // is_fundamental template<class _Tp> struct is_fundamental : public detail::_or_<is_arithmetic<_Tp>, is_void<_Tp>, is_null_pointer<_Tp> >::type {}; // is_object template<class _Tp> struct is_object : public detail::_not_< detail::_or_< is_function<_Tp>, is_reference<_Tp>, is_void<_Tp> > >::type {}; // is_scalar template<class _Tp> struct is_scalar : public detail::_or_<is_arithmetic<_Tp>, is_pointer<_Tp>, is_member_pointer<_Tp>, is_null_pointer<_Tp>/*, is_enum<_Tp>*/ >::type {}; // is_compound template<class _Tp> struct is_compound: public detail::_not_<is_fundamental<_Tp> >::type { }; 
Un lecteur attentif remarquera que la définition de is_enum est commentée. Le fait est que je n'ai pas trouvé de moyens de distinguer enum des autres types, mais je pense que cela est possible sans l'utilisation de macros dépendantes du compilateur. Peut-être qu'un lecteur attentif et compétent vous expliquera votre cheminement ou votre façon de penser à cet égard.
Pour déterminer le fait qu'un type est une classe, rien de plus n'est nécessaire maintenant:

 namespace detail { template <class _Tp, bool _IsReference> struct _is_class_helper { typedef integral_constant<bool, false> type; }; template <class _Tp> struct _is_class_helper<_Tp, false> { typedef integral_constant<bool, (is_scalar<_Tp>::value == bool(false)) //&& !is_union<_Tp>::value >::value && (is_array<_Tp>::value == bool(false)) && (is_void<_Tp>::value == bool(false)) && (is_function<_Tp>::value == bool(false))> type; }; } // is_class template<class _Tp> struct is_class : public detail::_is_class_helper<typename remove_cv<_Tp>::type, is_reference<_Tp>::value>::type { }; 

Et tout irait bien, mais l' union en C ++ ne peut pas être distinguée d'une classe dans le cas général. Parce qu'ils sont très similaires dans leurs "manifestations extérieures", et je n'ai pas pu vérifier les différences (par exemple, l'impossibilité d'hériter de l' union ) sans erreurs de compilation. Peut-être que quelqu'un vous dira une manœuvre délicate pour déterminer l' union lors de la compilation, puis is_class correspondra exactement à la norme.

Dans la dernière partie de ce chapitre, je parlerai de la façon dont std :: decay et std :: common_type ont été implémentés , ainsi que de ce qui reste à ajouter à type_traits .

Merci de votre attention.

Source: https://habr.com/ru/post/fr418347/


All Articles