
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. This is where
core.h is almost complete, but it would not be complete without
nullptr .
Link to GitHub with the result today for the impatient and non-readers:
')
Commits and constructive criticism are welcome
So let's continue.
Table of contents
IntroductionChapter 1. Viam supervadet vadensChapter 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endifChapter 3. Finding the perfect implementation of nullptrChapter 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 libraryChapter 5
...
Chapter 3. Finding the perfect implementation of nullptr
After the whole epic with nonstandard compiler macros and discoveries of the “wonderful” they presented, I could finally add
nullptr and it even warmed my soul. Finally, it will be possible to get rid of all these comparisons with 0 or even with
NULL .

Most programmers implement
nullptr as
#define nullptr 0
and this could be the end of this chapter. If you want
nullptr for
yourself , then just replace 0 with such a define, because in fact this is all that is required for correct operation.
Do not forget to really write a check, and then suddenly someone else will be with the following definition:
#ifndef nullptr #define nullptr 0 #else #error "nullptr defined already" #endif
The
#error preprocessor
directive will
generate an error with human-readable text when compiled, and yes, this is a standard directive, the use of which is rare but can be found.
But in this implementation, we miss one of the important points described in the standard, namely
std :: nullptr_t - a separate type, of which
nullptr is a constant instance. And the developers of chromium, when they also
tried to solve this problem (now there is already a newer compiler and a normal
nullptr ), defining it as a class that can convert to a pointer to any type. Since according to the standard, the size of
nullptr should be equal to the size of a pointer to
void (and
void * should also contain any pointer, except for pointers to a member of the class), we slightly “standardize” this implementation by adding an unused null pointer:
class nullptr_t_as_class_impl { public: nullptr_t_as_class_impl() { } nullptr_t_as_class_impl(int) { }
The conversion of this class to any pointer is due to a generic type operator, which is called if something is compared with
nullptr . Ie expression
char * my_pointer; if (my_pointer == nullptr) will actually be converted to
if (my_pointer == nullptr.operator char * ()) , which compares the pointer with 0. The second type operator is needed to convert
nullptr to pointers to class members. And here Borland C ++ Builder 6.0 has already “distinguished itself”, which unexpectedly decided that these two operators are identical and it can easily compare pointers to a member of a class and ordinary pointers to each other, therefore uncertainty arises every time such a
nullptr is compared with pointer (this is a bug, and perhaps it is not only in this compiler). We write a separate implementation for this case:
class nullptr_t_as_class_impl1 { public: nullptr_t_as_class_impl1() { } nullptr_t_as_class_impl1(int) { }
The advantages of this representation of
nullptr are that now there is a separate type for
std :: nullptr_t . Disadvantages? The
nullptr is lost during compilation and comparison through a ternary operator; the compiler cannot resolve it.
unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr;
And I want "and checkered and go." The decision comes to mind only one thing:
enum . The members of an enumeration in C ++ will have their own separate type, and will also be converted to
int without problems (and in fact are integer constants). This property of an enumeration member will help us, because the very “special” 0, which is used instead of
nullptr for pointers, is the most common
int . I haven’t met such implementation of
nullptr on the Internet, and, perhaps, it is also something bad, but I haven’t found ideas with what. We write the implementation:
#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)
As you can see here, a bit more code than just the
enum nullptr_t declaration with the member
nullptr = 0 . First, the definition of
NULL may not be. It should be defined in a
fairly solid list of standard headers , but as practice has shown, it is better to be safe and check for the presence of this macro. Secondly,
enum representation in C ++ according to the implementation-defined standard, i.e. the type of an enumeration can be represented by any integer types (with the proviso that these types cannot be greater than
int , if only
enum values
fit into it). For example, if you declare
enum test {_1, _2} the compiler can easily present it as
short and then it is quite possible that
sizeof ( test ) ! = Sizeof (void *) . In order for the implementation of
nullptr to meet the standard, you need to make sure that the size of the type that the compiler selects for
nullptr_t_as_enum will match the size of the pointer, i.e. essentially equal to
sizeof (void *) . To do this, use the
nullptr_t_as templates
... select an integer type that will be equal to the pointer size, and then set the maximum element value in our enumeration to the maximum value of this integer type.
I want to draw attention to the macro CHAR_BIT defined in the standard climits header. This macro is set to the number of bits in one char , i.e. The number of bits per byte on the current platform. A useful standard definition, which is undeservedly bypassed by developers sticking eights everywhere, although here and there in one byte there is anything but 8 bits .
And another feature is the assignment of
NULL as the value of the element
enum . Some compilers provide warning (and their concern can be understood) about the fact that
NULL is assigned to a “non-index”. We take out the standard
namespace to our local
ptrdiff_detail so as not to clutter up the rest of the namespace with them, and further, to reassure the compiler, we explicitly convert
NULL to
std :: ptrdiff_t - another for some reason little-used type in C ++, which serves to represent the result of arithmetic actions (subtraction) with pointers and is usually an alias of type
std :: size_t (
std :: intptr_t in C ++ 11).
SFINAE
Here, for the first time in my narration, we encounter the phenomenon of C ++ as
substitution failure is not an error (SFINAE) . Briefly, the essence of it is that when the compiler enumerates the appropriate function overloads for a particular call, it should check them all, and not stop after the first failure or after the first overload that was found. Hence, his
ambiguity messages appear when there are two overloads of the called function that are identical from the compiler's point of view, and also the ability of the compiler to select the most appropriate function overload for a particular call with specific parameters. This feature of the compiler allows you to do the lion's share of the whole template "magic" (by the way hello
std :: enable_if ), as well as being the basis of both boost and my library.
Since, as a result, we have several implementations of
nullptr, we use SFINAE to “select” the best at the compilation stage. We declare the types "yes" and "no" to check through the
sizeof probe functions declared below.
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(...); }
Here we will use the same principle as in the second chapter with the countof and its definition in terms of the sizeof of the return value (array of elements) of the template function 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)); };
What is going on here? First, the compiler “
iterates ” overloading the function
_is_convertable_to_void_ptr_tester with an argument of type
T and a value of
NULL (the value does not play a role, just
NULL must be reducible to type
T ). There are only two overloads - with the
void * type and with a
variable argument list (...) . Substituting the argument into each of these overloads, the compiler will choose the first if the type is cast to a pointer to
void , and the second if the cast cannot be performed. With the overload selected by the compiler, we will determine by
sizeof the size of the return value of the function, and since they are guaranteed to be different (
sizeof ( _no_type ) == 8 ,
sizeof ( _yes_type ) == 1 ), we can determine what overload the compiler has picked up and consequently converted whether our type is
void * or not.
We will continue to use the same programming pattern in order to determine whether the object of the chosen type for the representation
nullptr_t is
converted to any pointer (essentially
(T) ( STDEX_NULL ) and there is a future definition for
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) ); };
Of course, it is not possible to sort through all imaginable and inconceivable pointers and their combinations with
volatile and
const modifiers , so I limited myself to these 9 checks (two for class function pointers, one for
void pointer, seven for different type pointers), which is quite enough.
As mentioned above, some (* khe-khe * ... Borland Builder 6.0 ... * khe *) compilers do not distinguish between pointers to a type and a member of a class, so we will write a further check for this case and then select the desired implementation of
nullptr_t if need be.
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; };
And then it remains only to check the different implementations of
nullptr_t and select the appropriate compiler compiler.
Select the implementation of 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;
First, we check for the ability to represent
nullptr_t as a class, but since I did not find a universal compiler-
independent solution of how to check that an object of type can be a compile-time constant (by the way, I am open to suggestions for this account, because it is quite possible that this is possible), This option is always
checked (
_can_be_ct_constant is always
false ). Next, switch to checking the option with the view through the
enum . If it was not possible to submit this way either (the compiler cannot provide a pointer or
enum for some reason), then we try to represent it as an integer type (whose size will be equal to the size of the pointer to
void ). Well, if it didn’t work either, then select the implementation of the type
nullptr_t via
void * .
This place reveals much of the power of SFINAE in combination with C ++ templates, due to which it is possible to choose the necessary implementation, without resorting to compiler-dependent macros, and indeed to macros (as opposed to the boost where all this would be stuffed with
#ifdef #else checks endif ).
It only remains to define the type alias for
nullptr_t in the
namespace stdex and define for
nullptr (in order to meet another requirement of the standard that the address of
nullptr cannot be taken, and also that you can use
nullptr as a compile time constant).
namespace stdex { typedef detail::_nullptr_chooser::type nullptr_t; } #define nullptr (stdex::nullptr_t)(STDEX_NULL)
The end of the third chapter. In the
fourth chapter, I finally get to type_traits and what other bugs in the compilers I came across while developing.
Thank you for attention.