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

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 la première partie, nous avons examiné mon implémentation des modèles les plus simples de la bibliothèque standard, mais nous allons maintenant approfondir les modèles.

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

Poursuite de l'immersion dans le monde du "template magic" C ++.

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.2 À propos du nombre d'erreurs miraculeuses que le journal compile


Dans la première partie de ce chapitre, les modèles de base type_traits ont été introduits, mais quelques autres manquaient pour l'ensemble complet.

Par exemple, les modèles is_integral et is_floating_point étaient simplement nécessaires, qui sont en fait très trivialement définis - grâce à la spécialisation du modèle pour chaque type intégré. La question ne se pose ici qu'avec les "grands" types de long long . Le fait est que ce type intégré n'apparaît dans la norme de langage C ++ qu'à partir de la version 11. Et il serait logique de supposer que tout se résume à vérifier la version de la norme C ++ (ce qui est particulièrement difficile à déterminer de toute façon ), mais ce n'était pas là.

image Parce que, depuis 1999, la norme du langage C99 C existe dans laquelle les types long long int et unsigned long long int sont déjà présents (depuis 1999!), Et comme le langage C ++ a cherché à maintenir la compatibilité descendante avec le C pur, de nombreux compilateurs (qui généralement mixte C / C ++) l'a juste ajouté comme type fondamental avant même la publication de la norme C ++ 03. Autrement dit, la situation était que le type intégré est en fait (de C), mais il n'est pas décrit dans la norme C ++ et ne devrait pas être là. Et cela ajoute un peu plus de confusion à l'implémentation de la bibliothèque standard. Mais regardons le code:

namespace detail { template <class> struct _is_floating_point : public false_type {}; template<> struct _is_floating_point<float> : public true_type {}; template<> struct _is_floating_point<double> : public true_type {}; template<> struct _is_floating_point<long double> : public true_type {}; } template <class _Tp> struct is_floating_point : public detail::_is_floating_point<typename remove_cv<_Tp>::type> { }; 

Tout est clair avec le code ci-dessus - nous spécialisons le modèle pour les types à virgule flottante nécessaires, et, après avoir «effacé» les modificateurs de type, nous disons «oui» ou «non» au type qui nous est transmis. Viennent ensuite les types entiers:

 namespace detail { template <class> struct _is_integral_impl : public false_type {}; template<> struct _is_integral_impl<bool> : public true_type {}; template<> struct _is_integral_impl<char> : public true_type {}; template<> struct _is_integral_impl<wchar_t> : public true_type {}; template<> struct _is_integral_impl<unsigned char> : public true_type {}; template<> struct _is_integral_impl<unsigned short int> : public true_type {}; template<> struct _is_integral_impl<unsigned int> : public true_type {}; template<> struct _is_integral_impl<unsigned long int> : public true_type {}; #ifdef LLONG_MAX template<> struct _is_integral_impl<unsigned long long int> : public true_type {}; #endif template<> struct _is_integral_impl<signed char> : public true_type {}; template<> struct _is_integral_impl<short int> : public true_type {}; template<> struct _is_integral_impl<int> : public true_type {}; template<> struct _is_integral_impl<long int> : public true_type {}; #ifdef LLONG_MAX template<> struct _is_integral_impl<long long int> : public true_type {}; #endif template <class _Tp> struct _is_integral : public _is_integral_impl<_Tp> {}; template<> struct _is_integral<char16_t> : public true_type {}; template<> struct _is_integral<char32_t> : public true_type {}; template<> struct _is_integral<int64_t> : public true_type {}; template<> struct _is_integral<uint64_t> : public true_type {}; } template <class _Tp> struct is_integral : public detail::_is_integral<typename remove_cv<_Tp>::type> { }; 

Ici, vous devez vous arrêter un peu et réfléchir. Pour les "anciens" types entiers comme int , bool , etc. nous faisons les mêmes spécialisations qu'avec is_floating_point . Pour les "nouveaux" types long long int et son homologue non signé, nous définissons les surcharges uniquement s'il existe un LLONG_MAX define , qui a été défini en C ++ 11 (comme le premier standard C ++ compatible avec C99), et doit être défini dans le fichier d'en-tête climits comme maximum un grand nombre qui s'inscrit dans un objet de type long long int . Climits a également quelques définitions de macro supplémentaires (pour le plus petit nombre possible et les équivalents non signés), mais j'ai décidé d'utiliser cette macro, ce qui n'est pas important. L'important est que, contrairement à boost, dans cette implémentation, les "grands" types de C ne seront pas définis comme des constantes entières, bien qu'ils soient (éventuellement) présents dans le compilateur. Ce qui est plus important, ce sont les types char16_t et char32_t , qui ont également été introduits en C ++ 11, mais ils n'étaient pas déjà livrés en C99 (ils apparaissaient déjà simultanément avec C ++ dans la norme C11), et donc, dans les anciennes normes, leur définition peut être uniquement via un alias de type (par exemple typedef short char16_t , mais plus à ce sujet plus tard). Si tel est le cas, pour que la spécialisation de modèle gère correctement les situations lorsque ces types sont séparés (intégrés) et lorsqu'ils sont définis via typedef , une couche supplémentaire de détails de spécialisation de modèle :: _ is_integral est nécessaire .

Un fait intéressant est que dans certains anciens compilateurs, ces "grands" types C-timides ne sont pas des constantes intégrales . Ce qui peut être compris et même pardonné, car ces types ne sont pas standard pour C ++ jusqu'à 11 normes, et en général ils ne devraient pas être là. Mais ce qui est difficile à comprendre, c'est que ces types dans le dernier compilateur C ++ de la créativité Embarcadero (Embarcadero C ++ Builder), que C ++ 11 est censé prendre en charge, ne sont toujours pas constants dans leurs assemblages 32 bits (comme il y a 20 ans alors c'était Borland toujours vrai). Apparemment à cause de cela, y compris, la plupart de la bibliothèque C ++ 11 standard est manquante dans ces assemblages 32 bits (#include ratio? Chrono? Will cost). Embarcadero semble avoir décidé de forcer l'ère 64 bits avec la devise: «Voulez-vous C ++ 11 ou une norme plus récente? Construisez un programme 64 bits (et seulement clang, notre compilateur ne peut pas)! »

Après avoir terminé la procédure avec les types de langage fondamentaux, nous introduisons quelques modèles plus simples:

Modèles simples
 template <bool, class _Tp = detail::void_type> struct enable_if { }; template <class _Tp> struct enable_if<true, _Tp> { typedef _Tp type; }; template<class, class> struct is_same : public false_type { }; template<class _Tp> struct is_same<_Tp, _Tp> : public true_type//specialization { }; template <class _Tp> struct is_const : public false_type { }; template <class _Tp> struct is_const<const _Tp> : public true_type { }; template <class _Tp> struct is_const<const volatile _Tp> : public true_type { }; /// is_volatile template<class> struct is_volatile : public false_type { }; template<class _Tp> struct is_volatile<volatile _Tp> : public true_type { }; template<class _Tp> struct is_volatile<const volatile _Tp> : public true_type { }; 


Seul le fait que les modèles se spécialisent pour tous les modificateurs du type ( volatile et const volatile par exemple) est à noter ici, car certains compilateurs ont tendance à «perdre» l'un des modificateurs lors de l'expansion du modèle.

Séparément, je souligne l'implémentation de is_signed et is_unsigned :

 namespace detail { template<bool> struct _sign_unsign_chooser; template<class _Tp> struct _signed_comparer { static const bool value = _Tp(-1) < _Tp(0); }; template<class _Tp> struct _unsigned_comparer { static const bool value = _Tp(0) < _Tp(-1); }; template<bool Val> struct _cat_base : integral_constant<bool, Val> { // base class for type predicates }; template<> struct _sign_unsign_chooser<true>//integral { template<class _Tp> struct _signed : public _cat_base<_signed_comparer<typename remove_cv<_Tp>::type>::value> { }; template<class _Tp> struct _unsigned : public _cat_base<_unsigned_comparer<typename remove_cv<_Tp>::type>::value> { }; }; template<> struct _sign_unsign_chooser<false>//floating point { template<class _Tp> struct _signed : public is_floating_point<_Tp> { }; template<class _Tp> struct _unsigned : public false_type { }; }; } template<class T> struct is_signed { // determine whether T is a signed type static const bool value = detail::_sign_unsign_chooser<is_integral<T>::value>::template _signed<T>::value; typedef const bool value_type; typedef integral_constant<bool, is_signed::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; template<class T> struct is_unsigned { // determine whether T is an unsigned type static const bool value = detail::_sign_unsign_chooser<is_integral<T>::value>::template _unsigned<T>::value; typedef const bool value_type; typedef integral_constant<bool, is_unsigned::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; 

Lors de la mise en œuvre de cette partie, je suis entré dans une bataille inégale avec Borland C ++ Builder 6.0, qui ne voulait pas faire de ces deux modèles des héritiers de integr_constant , ce qui a finalement entraîné des dizaines d'erreur de compilation interne «imitant» le comportement integr_constant de ces modèles. Ici, peut-être, il vaut la peine de se battre et de trouver une sorte de dérivation délicate du type is_ * un * signé: integr_constant via des modèles, mais jusqu'à présent, j'ai reporté cette tâche comme une priorité. Ce qui est intéressant dans la section de code ci-dessus, c'est comment au moment de la compilation, il est déterminé que le type n'est pas signé / signé. Pour commencer, tous les types non entiers sont marqués et pour eux, le modèle va à une branche spécialisée distincte _sign_unsign_chooser avec l'argument de modèle false , qui à son tour renvoie toujours la valeur == false pour tous les types, sauf les types à virgule flottante standard (ils sont toujours signés pour des raisons évidentes, donc _signed :: value sera vrai ). Pour les types entiers, simples, mais plutôt divertissants, des vérifications sont effectuées. Ici, nous utilisons le fait que pour les types entiers non signés, lorsque le nombre diminue puis passe par un minimum (0 évidemment), un débordement se produit et le nombre acquiert sa valeur maximale possible.

Ce fait est bien connu, ainsi que le fait que pour les types signés, le débordement est un comportement non défini et que vous devez le surveiller (selon la norme, vous ne pouvez pas réduire une variable int inférieure à INT_MIN et espérer qu'à la suite du débordement, vous obtiendrez INT_MAX , pas 42 ou un disque dur formaté )

Nous écrivons _Tp (-1) <_Tp (0) pour vérifier le type "signe" en utilisant ce fait, puis pour les types non signés -1 "transforme" par débordement au nombre maximum de ce type, tandis que pour les types signés une telle comparaison sera effectuée sans débordement, et -1 sera comparé à 0.

Et le dernier pour aujourd'hui, mais loin du dernier "truc" de ma bibliothèque est l'implémentation de l' alignement_de :

 namespace detail { template <class _Tp> struct _alignment_of_trick { char c; _Tp t; _alignment_of_trick(); }; template <unsigned A, unsigned S> struct _alignment_logic_helper { static const std::size_t value = A < S ? A : S; }; template <unsigned A> struct _alignment_logic_helper<A, 0> { static const std::size_t value = A; }; template <unsigned S> struct _alignment_logic_helper<0, S> { static const std::size_t value = S; }; template< class _Tp > struct _alignment_of_impl { #if _MSC_VER > 1400 // // With MSVC both the build in __alignof operator // and following logic gets things wrong from time to time // Using a combination of the two seems to make the most of a bad job: // static const std::size_t value = (_alignment_logic_helper< sizeof(_alignment_of_trick<_Tp>) - sizeof(_Tp), __alignof(_Tp) >::value); #else static const std::size_t value = (_alignment_logic_helper< sizeof(_alignment_of_trick<_Tp>) - sizeof(_Tp), sizeof(_Tp) >::value); #endif typedef integral_constant<std::size_t, std::size_t(_alignment_of_impl::value)> type; private: typedef intern::type_traits_asserts check; typedef typename check::alignment_of_type_can_not_be_zero_assert< _alignment_of_impl::value != 0 >:: alignment_of_type_can_not_be_zero_assert_failed check1; // if you are there means aligment of type passed can not be calculated or compiler can not handle this situation (sorry, nothing can be done there) }; // borland compilers seem to be unable to handle long double correctly, so this will do the trick: struct _long_double_wrapper{ long double value; }; } template <class _Tp> struct alignment_of: public detail::_alignment_of_impl<_Tp>::type {}; template <class _Tp> struct alignment_of<_Tp&>: public alignment_of<_Tp*> {}; template<> struct alignment_of<long double>: public alignment_of<detail::_long_double_wrapper> {}; 

Microsoft a encore une fois excellé ici avec leur Visual Studio, qui, même avec une macro intégrée __alignof non standard intégrée , produit toujours des résultats incorrects lors de son utilisation.

Explication de boost
Les utilisateurs de Visual C ++ doivent noter que MSVC a différentes définitions de «alignement». Par exemple, considérez le code suivant:

 typedef long long align_t; assert(boost::alignment_of<align_t>::value % 8 == 0); align_t a; assert(((std::uintptr_t)&a % 8) == 0); char c = 0; align_t a1; assert(((std::uintptr_t)&a1 % 8) == 0); 

Dans ce code, même si boost :: alignement_al <align_t> signale que align_t a un alignement sur 8 octets, l'assertion finale échouera pour une génération 32 bits car a1 n'est pas aligné sur une limite de 8 octets. Notez que si nous avions utilisé le MSVC intrinsèque __alignof à la place de boost :: alignement_of, nous obtiendrions toujours le même résultat. En fait, les exigences d'alignement MSVC (et les promesses) ne s'appliquent vraiment qu'au stockage dynamique, et non à la pile.


Permettez-moi de vous rappeler ce que le modèle std :: alignement_of devrait faire - retourner une valeur qui représente les exigences pour le placement d'un élément de ce type en mémoire. Une petite distraction, puis un élément de chaque type a une sorte d'allocation de mémoire, et s'il est continu pour un tableau d'éléments, alors, par exemple, les classes peuvent bien avoir des «trous» entre les éléments membres de la classe ( sizeof class struct { char a;} ne sera probablement pas égal à 1, bien qu'il y ait 1 octet de tout à l'intérieur, car le compilateur l'alignera sur 1 + 3 octets pendant le processus d'optimisation).

Maintenant, regardons à nouveau le code. Nous déclarons la structure _alignment_of_trick , dans laquelle nous plaçons un élément du type en cours de vérification avec un «retrait» en mémoire de 1 octet. Et vérifiez l'alignement en soustrayant simplement la taille du type à vérifier de la taille de la structure résultante. Autrement dit, si le compilateur décide de «coller» un espace vide entre l'élément du type en cours de vérification et le caractère précédent, alors nous obtenons la valeur d'alignement du type dans la structure.

Ici aussi, l'assertion statique est d'abord rencontrée en tant que type. Ils sont déclarés comme:

 namespace intern { // since we have no static_assert in pre-C++11 we just compile-time assert this way: struct type_traits_asserts { template<bool> struct make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert; template<bool> struct make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert; template<bool> struct not_allowed_arithmetic_type_assert; template<bool> struct alignment_of_type_can_not_be_zero_assert; }; template<> struct type_traits_asserts::make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert<true> { typedef bool make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert_failed; }; template<> struct type_traits_asserts::make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert<true> { typedef bool make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert_failed; }; template<> struct type_traits_asserts::not_allowed_arithmetic_type_assert<true> { typedef bool not_allowed_arithmetic_type_assert_failed; }; template<> struct type_traits_asserts::alignment_of_type_can_not_be_zero_assert<true> { typedef bool alignment_of_type_can_not_be_zero_assert_failed; }; } 

En fait, ces modèles spécialisés sont nécessaires pour remplacer le static_assert de C ++ 11, qui se trouve à l'intérieur de la définition de classe. Ces assertions sont plus légères et hautement spécialisées que l'implémentation générale de STATIC_ASSERT du chapitre 2 , et vous permettent de ne pas faire glisser le fichier d'en-tête core.h dans type_traits .

image Beaucoup de motifs? Il y en aura plus! Nous allons nous attarder sur cela pour l'instant, car l'histoire fascinante de la combinaison de la programmation de modèles avec la technologie SFINAE, ainsi que la raison pour laquelle j'ai dû écrire un petit générateur de code, se poursuivra.

Merci de votre attention.

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


All Articles