
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é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 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 .

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) { }
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) { }
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;
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;
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.