📜 ⬆️ ⬇️

As I wrote the standard C ++ 11 library or why the boost is so scary. Chapter 4.2

We continue the adventure.

Summary of the previous parts


Due to limitations on the ability to use C ++ 11 compilers and from the lack of alternatives to boost, there was a desire to write our own implementation of the C ++ 11 standard library on top of the C ++ 98 / C ++ 03 library supplied with the compiler.

Were implemented static_assert , noexcept , countof , as well as, after considering all the non-standard defines and features of the compilers, there was information about the functionality that is supported by the current compiler. Included is its own implementation of nullptr , which is selected at compile time.

The time has come for type_traits and all this “special patterned magic”. In the first part, we looked at my implementation of the simplest templates of the standard library, but now we will climb "deeper" into the templates.
')
Link to GitHub with the result today for the impatient and non-readers:

Commits and constructive criticism are welcome

Continued immersion in the world of "template magic" C ++.

Table of contents


Introduction
Chapter 1. Viam supervadet vadens
Chapter 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Chapter 3. Finding the perfect implementation of nullptr
Chapter 4. C ++ Template "Magic"
.... 4.1 Starting small
.... 4.2 How many we get wonderful errors are compiled by the log
.... 4.3 Pointers and all-all-all
.... 4.4 What else is needed for the template library
Chapter 5
...

Chapter 4. C ++ Template "Magic". Continuation


4.2 How many marvelous errors to us are prepared by compilations of the log


In the first part of this chapter, the basic type_traits templates were introduced, but a few more were missing for the complete set.

For example, the is_integral and is_floating_point templates , which are actually very trivial defined, were simply necessary - through the template specialization for each built-in type. The question here only arose with "big" types of long long . The fact is that this type as built-in appears in the C ++ standard only from version 11. And it would be logical to assume that it all comes down to checking the version of the C ++ standard (which is definitely difficult to determine ), but it was not there.

image Because since 1999 there is a standard for the C language C99, in which the types long long int and unsigned long long int were already present (since 1999!), And since the C ++ language sought to maintain backward compatibility with pure C, many compilers (which were usually mixed C / C ++) just added it as a fundamental type even before the release of even the standard C ++ 03. That is, it turned out that the built-in type is in fact (from C), but it is not described in the C ++ standard and should not be there. And this brings a little more confusion to the implementation of the standard library. But let's look at the 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> { }; 

With the code above, everything is clear - we specialize the template into the necessary types with a floating point, and, after “cleaning” from type modifiers, we say “yes” or “no” to the type transferred to us. Next in line are the integer types:

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

Here it is necessary to stop and think a little. For “old” integer types like int , bool , etc. we do the same specialization as is_floating_point . For the “new” types of long long int and its unsigned counterpart, we define overloads only when there is a define LLONG_MAX , which was defined in C ++ 11 (as the first C ++ standard that is compatible with C99), and should be defined in the climits header file as maximum a large number that fits in a long long int object. There are also a few macro definitions in climits (for the minimum possible number and unsigned equivalents), but I decided to use this macro, which is not important. The important thing is that, unlike boost, in this implementation the “big” types from C will not be defined as integer constants, although they are (possibly) present in the compiler. What else is important is the types char16_t and char32_t , which were also introduced in C ++ 11, but they were not delivered in C99 (they appeared simultaneously with C ++ in C standard C11), and therefore in old standards their definition to be only through a type alias (for example, typedef short char16_t , but more on that later). If so, then in order for the template specialization to handle situations correctly and when these types are separate (built-in) and when they are defined via typedef , another layer of the detail specialization is required for the detail :: _ is_integral .

The interesting fact is that in some older compilers, these C-shnye "big" types are not an integral constant . What can be understood and even forgive, since these types are non-standard for C ++ up to 11 standards, and generally they should not be there. But what is difficult to understand is that these types in the newest C ++ creative compiler Embarcadero (Embarcadero C ++ Builder), which C ++ 11 supposedly supports, are still not an integral constant in their 32-bit builds (like 20 years ago , then it was Borland still true). Apparently because of this, including, most of the standard C ++ 11 library is missing in these 32-bit assemblies (#include the ratio? Chrono? You will manage). Embarcadero seems to have decided to force the onset of the 64-bit era with the motto: “Do you want a C ++ 11 or newer standard? Build a 64-bit program (and only clang, our compiler cannot)! ”.

Having finished the proceedings with the fundamental types of the language, we introduce some more simple patterns:

Simple templates
 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 { }; 


It deserves attention unless the fact that templates specialize for all type modifiers ( volatile and const volatile for example), since some compilers tend to “lose” one of the modifiers when a template is opened.

Separately, I highlight the implementation of is_signed and 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); } }; 

When implementing this part, I entered into an unequal fight with Borland C ++ Builder 6.0, which did not want to make these two templates the successors of integral_constant , which in the end, after dozens of internal compiler error, resulted in an “imitation” of the integral_constant behavior for these templates. Here, it may be worthwhile to still struggle and come up with some kind of clever derivation of the type is_ * un * signed: integral_constant through templates, but for the time being I have postponed this task as not a priority. Interestingly, in the above code section, it is determined at compile time that the type is unsigned / signed. To begin with, all non-integer types are thrown away and for them the template goes into a separate specialized _sign_unsign_chooser branch with the template argument false , which in turn always returns value == false for any types except standard floating point types (they are always signed for obvious reasons, so _signed :: value will be true ). For integer types, simple but so interesting checks are performed. Here we use the fact that for unsigned integer types, with decreasing and subsequent “passing” through a minimum (0 obviously) an overflow occurs and the number acquires its maximum possible value.

This fact is well known, as well as the fact that for sign types overflow is undefined behavior and you need to follow this (according to the standard, you cannot reduce the int variable less than INT_MIN and hope that as a result of the overflow you will get INT_MAX , not 42 or formatted hard disk ).

We write _Tp (-1) <_Tp (0) to check for “signedness” of the type using this fact, then for unsigned types, -1 is “transformed” through overflow to the maximum number of this type, whereas for sign-based ones, the comparison will be made without overflow, and will be compared to -1 with 0.

And the last one for today, but not the last “trick” in my library is the implementation of the alignment_of :

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

Here Microsoft again excelled with their Visual Studio, which even with a built-in nonstandard __alignof build-in macro still produces incorrect results when using it.

Explanation from boost
Visual C ++ users should not note that MSVC has varying definitions of "alignment". For example consider the following code:

 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); 

In this code, even though the boost has been set up, it can be a 32-bit build because it is not aligned with the 8-byte alignment. Note that we used the MSVC intrinsic __alignof in place of the boost :: we would still get the same result. In fact, for MSVC alignment requirements and


Let me remind you what the std :: alignment_of template should do - return a value that represents the requirements for placing an element of this type in memory. If we digress a little, then an element of each type has some kind of memory allocation, and if it is continuous for the array of elements, then, for example, classes may well have “holes” between the class members ( sizeof of the class struct { char a;} is most likely not equal to 1, although there is 1 byte of everything inside, since the compiler aligns it to 1 + 3 bytes in the optimization process).

And now let's look at the code again. Let's declare the _alignment_of_trick structure, in which we will place an “indent” from the memory of 1 byte element of the type being checked. And check the alignment simply by subtracting the size of the type being checked from the size of the resulting structure. In other words, if the compiler decides to “insert” the empty space between the element of the type being checked and the previous char , we will get the type alignment value in the structure.

Also here, static assert is first encountered as a type. They are declared as:

 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; }; } 

In fact, these specialized templates are needed to replace the static_assert from C ++ 11, which is located inside the class definition. Such assert are more lightweight and highly specialized than the general implementation of STATIC_ASSERT from chapter 2 , and allow you not to pull the header file core.h into type_traits .

image Many templates? Will be even more! We’ll stop on this for now, as the exciting story about combining template programming with the SFINAE technique will continue, as well as why I had to write a small code generator.

Thank you for attention.

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


All Articles