Comment ai-je écrit la bibliothèque C ++ 11 standard ou pourquoi boost est si effrayant. Chapitre 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. Ceci termine la description de core.h , mais il ne serait pas complet sans nullptr .

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

Alors continuons.

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 3. Recherche de l'implémentation nullptr parfaite


Après toute l'épopée avec des macros de compilateur non standard et les «merveilleuses» découvertes qu'elles ont présentées, j'ai finalement pu ajouter nullptr et cela a en quelque sorte réchauffé mon âme. Enfin, vous pouvez vous débarrasser de toutes ces comparaisons avec 0 ou même NULL .

image La plupart des programmeurs implémentent nullptr comme
#define nullptr 0 

et cela aurait pu terminer ce chapitre. Si vous voulez vous-même nullptr , remplacez simplement 0 par une telle définition, car c'est essentiellement tout ce qui est nécessaire pour un fonctionnement correct.

N'oubliez pas de vraiment faire un chèque, sinon tout à coup quelqu'un d'autre se retrouvera avec cette définition:

 #ifndef nullptr #define nullptr 0 #else #error "nullptr defined already" #endif 

La directive de préprocesseur #error produira une erreur avec du texte lisible par l'homme lors de la compilation, et, oui, il s'agit d'une directive standard, dont l'utilisation est rare, mais peut être trouvée.

Mais dans une telle implémentation, nous manquons l'un des points importants décrits dans la norme, à savoir std :: nullptr_t - un type séparé, dont une instance constante est nullptr . Et les développeurs de chrome ont également essayé de résoudre ce problème (il existe maintenant un compilateur plus récent et un nullptr normal) le définissant comme une classe qui peut être convertie en pointeur vers n'importe quel type. Étant donné que, selon la norme, la taille de nullptr doit être égale à la taille du pointeur vers void (et void * doit également contenir n'importe quel pointeur, à l'exception des pointeurs vers un membre de la classe), nous «normalisons» cette implémentation en ajoutant un pointeur null inutilisé:

 class nullptr_t_as_class_impl { public: nullptr_t_as_class_impl() { } nullptr_t_as_class_impl(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } // Make nullptr convertible to any member pointer type. template<typename C, typename T> operator TC::*() { return 0; } bool operator==(nullptr_t_as_class_impl) const { return true; } bool operator!=(nullptr_t_as_class_impl) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl nullptr_t; #define nullptr nullptr_t(0) 

La conversion de cette classe en n'importe quel pointeur est due à l'opérateur de modèle du type, qui est appelé si quelque chose est comparé à nullptr . Autrement dit, l'expression char * my_pointer; if (my_pointer == nullptr) sera réellement converti en if (my_pointer == nullptr.operator char * ()) , qui compare le pointeur à 0. L'opérateur de second type est nécessaire pour convertir nullptr en pointeurs en membres de classe. Et ici, Borland C ++ Builder 6.0 s'est «distingué», qui a décidé de manière inattendue que ces deux opérateurs sont identiques et peuvent facilement comparer des pointeurs vers un membre de la classe et des pointeurs réguliers, il y a donc une incertitude à chaque fois qu'un tel nullptr est comparé à pointeur (c'est un bug, et ce n'est peut-être pas seulement avec ce compilateur). Nous écrivons une implémentation distincte pour ce cas:

 class nullptr_t_as_class_impl1 { public: nullptr_t_as_class_impl1() { } nullptr_t_as_class_impl1(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } bool operator==(nullptr_t_as_class_impl1) const { return true; } bool operator!=(nullptr_t_as_class_impl1) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl1 nullptr_t; #define nullptr nullptr_t(0) 

Les avantages de cette vue nullptr sont qu'il existe désormais un type distinct pour std :: nullptr_t . Inconvénients? La constante nullptr est perdue lors de la compilation et de la comparaison via l'opérateur ternaire, le compilateur ne peut pas la résoudre.

 unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr; //  ,     ':'    STATIC_ASSERT(nullptr == nullptr && !(nullptr != nullptr), nullptr_should_be_equal_itself); //  , nullptr      

Et je veux "et les dames et c'est parti." La solution ne vient à l'esprit que d'une seule: l' énumération . Les membres de l'énumération en C ++ auront leur propre type séparé et seront également convertis en int sans aucun problème (et en fait ce sont des constantes entières). Cette propriété d'un membre d'énumération nous sera utile, car le 0 très «spécial» qui est utilisé à la place de nullptr pour les pointeurs est l' int . Le plus courant. Je n'ai pas vu une telle implémentation de nullptr sur Internet, et c'est peut-être aussi quelque chose de mauvais, mais je n'avais aucune idée pourquoi. Écrivons une implémentation:

 #ifdef NULL #define STDEX_NULL NULL #else #define STDEX_NULL 0 #endif namespace ptrdiff_detail { using namespace std; } template<bool> struct nullptr_t_as_ulong_type { typedef unsigned long type; }; template<> struct nullptr_t_as_ulong_type<false> { typedef unsigned long type; }; template<bool> struct nullptr_t_as_ushort_type { typedef unsigned short type; }; template<> struct nullptr_t_as_ushort_type<false> { typedef nullptr_t_as_long_type<sizeof(unsigned long) == sizeof(void*)>::type type; }; template<bool> struct nullptr_t_as_uint_type { typedef unsigned int type; }; template<> struct nullptr_t_as_uint_type<false> { typedef nullptr_t_as_short_type<sizeof(unsigned short) == sizeof(void*)>::type type; }; typedef nullptr_t_as_uint_type<sizeof(unsigned int) == sizeof(void*)>::type nullptr_t_as_uint; enum nullptr_t_as_enum { _nullptr_val = ptrdiff_detail::ptrdiff_t(STDEX_NULL), _max_nullptr = nullptr_t_as_uint(1) << (CHAR_BIT * sizeof(void*) - 1) }; typedef nullptr_t_as_enum nullptr_t; #define nullptr nullptr_t(STDEX_NULL) 

Comme vous pouvez le voir ici, un peu plus de code que de simplement déclarer enum nullptr_t avec le membre nullptr = 0 . Premièrement, il peut ne pas y avoir de définitions NULL . Il doit être défini dans une liste assez solide d'en-têtes standard , mais comme la pratique l'a montré, il est préférable de le jouer en toute sécurité et de vérifier cette macro. Deuxièmement, la représentation de l' énumération en C ++ selon la norme définie par l'implémentation, c'est-à-dire le type d'énumération peut être représenté par n'importe quel type d'entier (à condition que ces types ne puissent pas être supérieurs à int , à condition que les valeurs d' énumération y correspondent ). Par exemple, si vous déclarez enum test {_1, _2}, le compilateur peut facilement le représenter comme court, et alors il est fort possible que sizeof ( test ) ! = Sizeof (void *) . Pour que l'implémentation nullptr soit conforme à la norme, vous devez vous assurer que la taille du type que le compilateur choisit pour nullptr_t_as_enum correspond à la taille du pointeur, c'est-à-dire essentiellement de taille égale (vide *) . Pour ce faire, à l'aide des modèles nullptr_t_as ... , sélectionnez un type entier qui sera égal à la taille du pointeur, puis définissez la valeur maximale de l'élément dans notre énumération sur la valeur maximale de ce type entier.
Je veux faire attention à la macro CHAR_BIT définie dans l'en-tête des climats standard. Cette macro est définie sur le nombre de bits dans un caractère, c'est-à-dire le nombre de bits par octet sur la plateforme actuelle. Une définition standard utile que les développeurs contournent sans raison en collant des huit partout, bien qu'à certains endroits dans un octet il n'y ait pas du tout 8 bits .

Et une autre caractéristique est l'affectation de NULL comme valeur de l'élément enum . Certains compilateurs donnent un avertissement (et leur inquiétude peut être comprise) sur le fait que NULL est affecté au "non-indexeur". Nous retirons l' espace de noms standard à notre ptrdiff_detail local, afin de ne pas encombrer le reste de l'espace de noms, puis, pour calmer le compilateur, nous convertissons explicitement NULL en std :: ptrdiff_t - un autre type en quelque sorte sous-utilisé en C ++, qui sert à représenter le résultat des opérations arithmétiques (soustraction) avec des pointeurs et est généralement un alias de type std :: size_t ( std :: intptr_t en C ++ 11).

SFINAE


Ici, pour la première fois dans mon histoire, nous sommes confrontés à un tel phénomène en C ++ car l' échec de substitution n'est pas une erreur (SFINAE) . En bref, l'essentiel est que lorsque le compilateur «passe par» les surcharges de fonctions appropriées pour un appel particulier, il doit les vérifier toutes et ne pas s'arrêter après la première défaillance ou après la première surcharge appropriée. D'où son message sur l' ambiguïté , lorsqu'il y a deux surcharges de la fonction appelée qui sont identiques du point de vue du compilateur, ainsi que la capacité du compilateur à sélectionner la surcharge de fonction la plus précise pour un appel spécifique avec des paramètres spécifiques. Cette fonctionnalité du compilateur vous permet de faire la part du lion de tout le modèle «magique» (en passant hi std :: enable_if ), et c'est également la base de boost et de ma bibliothèque.

Puisque, par conséquent, nous avons plusieurs implémentations nullptr, nous utilisons SFINAE «select» le meilleur au stade de la compilation. Nous déclarons les types «oui» et «non» pour vérifier la taille des fonctions de sonde déclarées ci-dessous.

 namespace nullptr_detail { typedef char _yes_type; struct _no_type { char padding[8]; }; struct dummy_class {}; _yes_type _is_convertable_to_void_ptr_tester(void*); _no_type _is_convertable_to_void_ptr_tester(...); typedef void(nullptr_detail::dummy_class::*dummy_class_f)(int); typedef int (nullptr_detail::dummy_class::*dummy_class_f_const)(double&) const; _yes_type _is_convertable_to_member_function_ptr_tester(dummy_class_f); _no_type _is_convertable_to_member_function_ptr_tester(...); _yes_type _is_convertable_to_const_member_function_ptr_tester(dummy_class_f_const); _no_type _is_convertable_to_const_member_function_ptr_tester(...); template<class _Tp> _yes_type _is_convertable_to_ptr_tester(_Tp*); template<class> _no_type _is_convertable_to_ptr_tester(...); } 

Ici, nous utiliserons le même principe que dans le deuxième chapitre avec countof et sa définition à travers sizeof de la valeur de retour (tableau d'éléments) de la fonction modèle COUNTOF_REQUIRES_ARRAY_ARGUMENT .

 template<class T> struct _is_convertable_to_void_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_void_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; 

Que se passe-t-il ici? Tout d'abord, le compilateur « itère » les surcharges de la fonction _is_convertable_to_void_ptr_tester avec un argument de type T et une valeur de NULL (la valeur ne joue pas de rôle, seul NULL doit être de type- T ). Il n'y a que deux surcharges - avec le type void * et avec la liste d'arguments variables (...) . En substituant un argument à chacune de ces surcharges, le compilateur sélectionnera le premier si le type est converti en un pointeur vers void , et le second si le transtypage ne peut pas être effectué. Avec la surcharge sélectionnée par le compilateur, nous utilisons sizeof pour déterminer la taille de la valeur retournée par la fonction, et comme elles sont garanties différentes ( sizeof ( _no_type ) == 8 , sizeof ( _yes_type ) == 1 ), nous pouvons déterminer la taille de la surcharge que le compilateur a détectée et donc convertie que notre type soit nul * ou non.

Nous appliquerons davantage le même modèle de programmation afin de déterminer si un objet du type de notre choix pour représenter nullptr_t est converti en n'importe quel pointeur (essentiellement (T) ( STDEX_NULL ) est la future définition de nullptr ).

 template<class T> struct _is_convertable_to_member_function_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)) && (sizeof(nullptr_detail::_is_convertable_to_const_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class NullPtrType, class T> struct _is_convertable_to_any_ptr_impl_helper { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_ptr_tester<T>((NullPtrType) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class T> struct _is_convertable_to_any_ptr_impl { static const bool value = _is_convertable_to_any_ptr_impl_helper<T, int>::value && _is_convertable_to_any_ptr_impl_helper<T, float>::value && _is_convertable_to_any_ptr_impl_helper<T, bool>::value && _is_convertable_to_any_ptr_impl_helper<T, const bool>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile float>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile const double>::value && _is_convertable_to_any_ptr_impl_helper<T, nullptr_detail::dummy_class>::value; }; template<class T> struct _is_convertable_to_ptr_impl { static const bool value = ( _is_convertable_to_void_ptr_impl<T>::value == bool(true) && _is_convertable_to_any_ptr_impl<T>::value == bool(true) && _is_convertable_to_member_function_ptr_impl<T>::value == bool(true) ); }; 

Bien sûr, il n'est pas possible d'itérer sur tous les pointeurs imaginables et inconcevables et leurs combinaisons avec des modificateurs volatils et const , donc je me suis limité à seulement ces 9 vérifications (deux sur les pointeurs vers les fonctions de classe, une sur le pointeur vers void , sept sur les pointeurs vers différents types), ce qui est assez suffisant.

Comme mentionné ci-dessus, certains compilateurs (* khe-khe * ... Borland Builder 6.0 ... * khe *) ne distinguent pas les pointeurs vers un type et un membre d'une classe, nous allons donc écrire une autre vérification d'aide pour ce cas afin que nous puissions ensuite sélectionner l'implémentation souhaitée de nullptr_t via la classe si besoin.

 struct _member_ptr_is_same_as_ptr { struct test {}; typedef void(test::*member_ptr_type)(void); static const bool value = _is_convertable_to_void_ptr_impl<member_ptr_type>::value; }; template<bool> struct _nullptr_t_as_class_chooser { typedef nullptr_detail::nullptr_t_as_class_impl type; }; template<> struct _nullptr_t_as_class_chooser<false> { typedef nullptr_detail::nullptr_t_as_class_impl1 type; }; 

Et puis il ne reste plus qu'à vérifier les différentes implémentations de nullptr_t et choisir le compilateur approprié pour le compilateur.

Choix de l'implémentation nullptr_t
 template<bool> struct _nullptr_choose_as_int { typedef nullptr_detail::nullptr_t_as_int type; }; template<bool> struct _nullptr_choose_as_enum { typedef nullptr_detail::nullptr_t_as_enum type; }; template<bool> struct _nullptr_choose_as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type type; }; template<> struct _nullptr_choose_as_int<false> { typedef nullptr_detail::nullptr_t_as_void type; }; template<> struct _nullptr_choose_as_enum<false> { struct as_int { typedef nullptr_detail::nullptr_t_as_int nullptr_t_as_int; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_int>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_int>::value; }; typedef _nullptr_choose_as_int<as_int::_is_convertable_to_ptr == bool(true) && as_int::_equal_void_ptr == bool(true)>::type type; }; template<> struct _nullptr_choose_as_class<false> { struct as_enum { typedef nullptr_detail::nullptr_t_as_enum nullptr_t_as_enum; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_enum>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_enum>::value; static const bool _can_be_ct_constant = true;//_nullptr_can_be_ct_constant_impl<nullptr_t_as_enum>::value; }; typedef _nullptr_choose_as_enum<as_enum::_is_convertable_to_ptr == bool(true) && as_enum::_equal_void_ptr == bool(true) && as_enum::_can_be_ct_constant == bool(true)>::type type; }; struct _nullptr_chooser { struct as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type nullptr_t_as_class; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_class>::value; static const bool _can_be_ct_constant = _nullptr_can_be_ct_constant_impl<nullptr_t_as_class>::value; }; typedef _nullptr_choose_as_class<as_class::_equal_void_ptr == bool(true) && as_class::_can_be_ct_constant == bool(true)>::type type; }; 


Tout d'abord, nous vérifions la possibilité de représenter nullptr_t comme une classe, mais comme je n'ai pas trouvé de compilateur universel d'une solution indépendante , je n'ai pas trouvé d'objet type pouvant être une constante de temps de compilation (soit dit en passant, je suis ouvert aux suggestions à ce sujet, car il est probable que cela soit possible), cette option est toujours cochée ( _can_be_ct_constant est toujours false ). Ensuite, nous passons à la vérification de la variante avec la vue à travers l' énumération . S'il n'était toujours pas possible de présenter (le compilateur ne peut pas présenter un pointeur via enum ou la taille est en quelque sorte incorrecte), alors nous essayons de le représenter comme un type entier (dont la taille sera égale à la taille du pointeur à annuler ). Eh bien, même si cela n'a pas fonctionné, nous sélectionnons une implémentation du type nullptr_t via void * .

À ce stade, la majeure partie de la puissance de SFINAE en combinaison avec les modèles C ++ est révélée, grâce à laquelle il est possible de choisir l'implémentation nécessaire sans recourir à des macros dépendantes du compilateur, et en fait à des macros (contrairement à boost où tout cela serait bourré de contrôles #ifdef #else # endif ).

Il ne reste plus qu'à définir un alias de type pour nullptr_t dans l' espace de noms stdex et un définir pour nullptr (afin de se conformer à une autre exigence standard que l'adresse nullptr ne peut pas être prise, ainsi que d'utiliser nullptr comme constante de temps de compilation).

 namespace stdex { typedef detail::_nullptr_chooser::type nullptr_t; } #define nullptr (stdex::nullptr_t)(STDEX_NULL) 

La fin du troisième chapitre. Dans le quatrième chapitre, j'arrive enfin à type_traits et aux autres bugs des compilateurs que j'ai rencontrés lors du développement.

Merci de votre attention.

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


All Articles