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

    Yes - yes, with this motto I rushed into battle.

    Summary of the previous parts


    Due to restrictions 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 standard C ++ 11 library on top of the C ++ 98 / C ++ 03 library supplied with the compiler.

    In addition to the standard header files type_traits , thread , mutex , chrono were also added nullptr.h that implements std :: nullptr_t and core.h where macros related to compiler-specific functionality were taken out, as well as extending the standard library.

    Link to GitHub with the result for today for impatient and non-readers:

    Commits and constructive criticism are welcome

    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 realization nullptr
    Chapter 4. Patterned "magic» the C ++
    .... 4.1 start small
    .... 4.2 About how many mistakes we Wonderful compilations are prepared by the log
    .... 4.3 Pointers and all-all-all
    .... 4.4 What else is needed for the sample library
    Chapter 5.
    ...

    Chapter 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif


    After all the code was slightly combed and divided by “standard” headers into a separate namespace stdex, I started filling in type_traits , nullptr.h and along the same core.h , which contained macros to determine the version of the standard used by the compiler and support it “ Nativenullptr , char16_t , char32_t and static_assert .

    In theory, everything is simple - according to the C ++ standard (Section 14.8), the __cplusplus macro should be defined by the compiler and correspond to the version of the supported standard:

    C++ pre-C++98:  #define __cplusplus 1
    C++98:          #define __cplusplus 199711L
    C++98 + TR1:    #define __cplusplus 199711L // ???
    C++11:          #define __cplusplus 201103L
    C++14:          #define __cplusplus 201402L
    C++17:          #define __cplusplus 201703L

    accordingly, the code to determine if support is trivial:

    #if (__cplusplus >= 201103L) // стандарт C++ 11 или выше#define _STDEX_NATIVE_CPP11_SUPPORT // есть поддержка 11 стандарта (nullptr, static_assert)#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT // есть встроенные типы char16_t, char32_t#endif

    imageIn fact, not everything is so simple and now interesting crutches with a rake begin.

    First of all, not everyone, or more precisely, not one of the compilers, implements the next standard completely and immediately. For example, in Visual Studio 2013 there was no constexpr for a very long time, and it was stated that it supports C ++ 11 - with the reservation that the implementation is not complete. That is, auto - please, static_assert is just as easy (from earlier MS VS), but constexpr is not . Secondly, not all compilers (and this surprises even more) correctly expose this define and update it in a timely manner. Unexpectedly, in the same compiler Visual Studio did not change the version of the defaults __cplusplusright from the very first versions of the compiler, although full support for C ++ 11 has long been announced (which is also not true, for which some rays of dissatisfaction are called for them - as soon as they talk about the specific functionality of the “new” 11 standard, the developers immediately say that there is no C99 preprocessor, yet other "features"). And the situation is further aggravated by the fact that according to the standard, compilers are allowed to set this define to different values ​​from the values ​​above, if they do not fully meet the stated standards. It would be logical to assume, for example, such a development of defines for a given macro (with the introduction of a new functionality, increase the number behind the define data):

    standart C++98:          #define __cplusplus 199711L // C++98
    standart C++98 + TR1:    #define __cplusplus 200311L // C++03
    nonstandart C++11:       #define __cplusplus 200411L // C++03 + auto and dectype
    nonstandart C++11:       #define __cplusplus 200511L // C++03 + auto, dectype and constexpr(partly)
    ...
    standart C++11:          #define __cplusplus 201103L // C++11

    But at the same time, no one of the main popular compilers is worn out with this feature.

    Because of all this (I’m not afraid of this word), the mess now for each non-standard compiler has to write its own specific checks in order to find out which C ++ standard and in what volume it supports. The good news is that we need to learn about just a few functions of the compiler to work correctly. First, we now add a version check for Visual Studio through the _MSC_VER macro unique to this compiler .. Since in my arsenal of supported compilers there is also C ++ Borland Builder 6.0, the developers of which, in turn, were very keen to maintain compatibility with Visual Studio (including its “features” and bugs), this macro is also there suddenly. For clang-compatible compilers, there is a non-standard macro __has_feature ( feature_name ) , which allows you to find out if the compiler has support for this or that functionality. As a result, the code swells up to:

    #ifndef __has_feature#define __has_feature(x) 0 // Compatibility with non-clang compilers.#endif// Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr#if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation#define _STDEX_NATIVE_CPP11_SUPPORT#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif

    Want to cover more compilers? Add checks for Codegear C ++ Builder, which is Borland's successor (in its worst manifestations, but more on that later):

    #ifndef __has_feature#define __has_feature(x) 0 // Compatibility with non-clang compilers.#endif// Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr#if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation#define _STDEX_NATIVE_CPP11_SUPPORT#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT)#if ((__cplusplus > 199711L) || defined(__CODEGEARC__))#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#endif

    It is also worth noting that since Visual Studio already supports nullptr from the _MSC_VER 1600 compiler version , as well as the built-in char16_t and char32_t types , we need to handle this correctly. Some more checks added:

    #ifndef __has_feature#define __has_feature(x) 0 // Compatibility with non-clang compilers.#endif// Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr#if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation#define _STDEX_NATIVE_CPP11_SUPPORT#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT)#if ((__cplusplus > 199711L) || defined(__CODEGEARC__))#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#endif#if ((!defined(_MSC_VER) || _MSC_VER < 1600) && !defined(_STDEX_NATIVE_CPP11_SUPPORT))#define _STDEX_IMPLEMENTS_NULLPTR_SUPPORT#else#define _STDEX_NATIVE_NULLPTR_SUPPORT#endif#if (_MSC_VER >= 1600)#ifndef _STDEX_NATIVE_CPP11_TYPES_SUPPORT#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#endif

    At the same time, we will also check for support for C ++ 98, since for compilers without it some header files of the standard library will not exist, and we can’t check the absence of them using the means of the compiler.

    Full version
    #ifndef __has_feature#define __has_feature(x) 0 // Compatibility with non-clang compilers.#endif// Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr#if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation#define _STDEX_NATIVE_CPP11_SUPPORT#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT)#if ((__cplusplus > 199711L) || defined(__CODEGEARC__))#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#endif#if ((!defined(_MSC_VER) || _MSC_VER < 1600) && !defined(_STDEX_NATIVE_CPP11_SUPPORT))#define _STDEX_IMPLEMENTS_NULLPTR_SUPPORT#else#define _STDEX_NATIVE_NULLPTR_SUPPORT#endif#if (_MSC_VER >= 1600)#ifndef _STDEX_NATIVE_CPP11_TYPES_SUPPORT#define _STDEX_NATIVE_CPP11_TYPES_SUPPORT#endif#endif#if _MSC_VER // Visual C++ fallback#define _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT#define _STDEX_CDECL __cdecl#if (__cplusplus >= 199711L)#define _STDEX_NATIVE_CPP_98_SUPPORT#endif#endif// C++ 98 check:#if ((__cplusplus >= 199711L) && ((defined(__INTEL_COMPILER) || defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4))))))    #ifndef _STDEX_NATIVE_CPP_98_SUPPORT#define _STDEX_NATIVE_CPP_98_SUPPORT#endif#endif


    And now volumetric configs from boost in which many hardworking developers have written out all these compiler-dependent macros begin to appear in memory and of them made up a map of what is supported and what is not a specific compiler for a specific version, from which I personally feel uneasy, I never want to look at it and not touch more. But the good news is that you can stop there. At least this is enough for me to support most popular compilers, but if you find an inaccuracy or want to add another compiler, I will only be happy to accept a pull request.

    I consider it a great achievement compared to the boost that I managed to keep the compiler-related macros from sprawling along the code, which makes the code cleaner and easier to understand, and not to load dozens of configuration files for each OS and for each compiler. We will talk about the disadvantages of this approach a little later.

    At this stage, we can already begin to connect the missing functionality of the 11 standard, and the first thing we introduce is static_assert .

    static_assert


    We define the StaticAssertion structure , which will take a boolean value as a template parameter - there will be our condition, if you fail to do this (the expression is set to false ), a non-specialized template compilation error will occur. And another dummy structure for receiving sizeof ( StaticAssertion ) .

    namespace stdex
    {
        namespace detail {
            template <bool>
            structStaticAssertion;template <>
            structStaticAssertion<true>
            {
            }; // StaticAssertion<true>template<int i>
            structStaticAssertionTest
            {
            }; // StaticAssertionTest<int>
        }
    }
    

    further macro magic

    #ifdef _STDEX_NATIVE_CPP11_SUPPORT#define STATIC_ASSERT(expression, message) static_assert((expression), #message)#else// no C++11 support#define CONCATENATE(arg1, arg2)   CONCATENATE1(arg1, arg2)#define CONCATENATE1(arg1, arg2)  CONCATENATE2(arg1, arg2)#define CONCATENATE2(arg1, arg2)  arg1##arg2#define STATIC_ASSERT(expression, message)\
        struct CONCATENATE(__static_assertion_at_line_, __LINE__)\
        {\
            stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\
        };\
        typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__)
        #ifndef _STDEX_NATIVE_NULLPTR_SUPPORT#define static_assert(expression, message) STATIC_ASSERT(expression, ERROR_MESSAGE_STRING)#endif#endif

    using:

    STATIC_ASSERT(sizeof(void*) == 4, non_x32_platform_is_unsupported);
    

    An important difference between my implementation and the standard one is that there is no overload of this keyword without a message to the user. This is due to the fact that in C ++ it is impossible to define several defines with a different number of arguments but with the same name, and the implementation without a message is much less useful than the selected option. This feature leads to the fact that in fact STATIC_ASSERT in my implementation is a version added already in C ++ 11.
    We will understand in order what happened. As a result of checks of __cplusplus versions and non-standard compiler macros, we have information on support of C ++ 11 in sufficient volume (and therefore static_assert ), expressed by the _STDEX_NATIVE_CPP11_SUPPORT define . Therefore, if this macro is defined we can simply use the standard static_assert :

    #ifdef _STDEX_NATIVE_CPP11_SUPPORT#define STATIC_ASSERT(expression, message) static_assert((expression), #message)

    Note that the second parameter of the STATIC_ASSERT macro is not a string literal at all and therefore, using the preprocessor operator #, we will convert the message parameter to a string for transfer to the standard static_assert .
    If we don’t have support from the compiler, then we’ll proceed to our own implementation. To begin with, we will declare auxiliary macros for sticking strings together (the preprocessor operator ## is responsible for this).

    #define CONCATENATE(arg1, arg2)   CONCATENATE1(arg1, arg2)#define CONCATENATE1(arg1, arg2)  CONCATENATE2(arg1, arg2)#define CONCATENATE2(arg1, arg2)  arg1##arg2

    I specifically did not use just # define CONCATENATE ( arg1 , arg2 ) arg1 ## arg2 in order to be able to pass the result of the same CONCATENATE macro as an argument to arg1 and arg2 .
    Next, we declare a structure with a beautiful name __static_assertion_at_line_ {line number} (the macro __LINE__ is also defined by the standard and should be expanded to the line number on which it was called), and inside this structure we add a field of our type StaticAssertion named STATIC_ASSERTION_FAILED_AT_LINE_ {line number} _WITH __ { error messages from the caller}.

    #define STATIC_ASSERT(expression, message)\
    struct CONCATENATE(__static_assertion_at_line_, __LINE__)\
    {\
        stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\
    };\
    typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__)
    

    With a template parameter in StaticAssertion, we pass an expression that is checked in STATIC_ASSERT , leading it to bool . Finally, in order to avoid creating local variables and zero-overhead checking the user condition, an alias is declared for the type StaticAssertionTest <sizeof ({structure name declared above) with the name __static_assertion_test_at_line_ {line number}.

    All beauty with naming is needed only to make it clear from the compilation error that this is the result of assert, and not just an error, but also to display an error message that was given for this assert. The trick with sizeof is needed to force the compiler to instantiate the template class.StaticAssertion , which is inside the newly declared structure, and thus check the condition passed to assert.

    STATIC_ASSERT issue results
    GCC:
    30:103: error: field 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' has incomplete type 'stdex::detail::StaticAssertion<false>'
    25:36: note: in definition of macro 'CONCATENATE2'
    23:36: note: in expansion of macro 'CONCATENATE1'
    30:67: note: in expansion of macro 'CONCATENATE'
    24:36: note: in expansion of macro 'CONCATENATE2'
    23:36: note: in expansion of macro 'CONCATENATE1'
    30:79: note: in expansion of macro 'CONCATENATE'
    24:36: note: in expansion of macro 'CONCATENATE2'
    23:36: note: in expansion of macro 'CONCATENATE1'
    30:91: note: in expansion of macro 'CONCATENATE'
    36:3: note: in expansion of macro 'STATIC_ASSERT'

    Borland C++ Builder:
    [C++ Error] stdex_test.cpp(36): E2450 Undefined structure 'stdex::detail::StaticAssertion<0>'
    [C++ Error] stdex_test.cpp(36): E2449 Size of 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' is unknown or zero
    [C++ Error] stdex_test.cpp(36): E2450 Undefined structure 'stdex::detail::StaticAssertion<0>'

    Visual Studio:
    Error C2079 'main::__static_assertion_at_line_36::STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' uses undefined struct 'stdex::detail::StaticAssertion<__formal>' stdex_test c:\users\user\documents\visual studio 2015\projects\stdex_test\stdex_test\stdex_test.cpp 36


    The second "trick" that I wanted to have, while absent in the standard is the countof - counting the number of elements in the array. Sichniki love to declare this macro through sizeof (arr) / sizeof (arr [0]), but we will go further.

    countof


    #ifdef _STDEX_NATIVE_CPP11_SUPPORT#include<cstddef>namespace stdex
    {
        namespace detail
        {
            template <classT, std::size_t N>
            constexprstd::size_t _my_countof(T const (&)[N]) noexcept
            {
                return N;
            }
        } // namespace detail
    }
    #define countof(arr) stdex::detail::_my_countof(arr)#else//no C++11 support#ifdef _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT // Visual C++ fallback#include<stdlib.h>#define countof(arr) _countof(arr)#elif defined(_STDEX_NATIVE_CPP_98_SUPPORT)// C++ 98 trick#include<cstddef>template <typename T, std::size_t N>
    char(&COUNTOF_REQUIRES_ARRAY_ARGUMENT(T(&)[N]))[N];
    #define countof(x) sizeof(COUNTOF_REQUIRES_ARRAY_ARGUMENT(x))#else#define countof(arr) sizeof(arr) / sizeof(arr[0])#endif

    For compilers with constexpr support, we will announce the constexpr version of this template (which is absolutely not necessary, in fact, for all standards, implementation through the COUNTOF_REQUIRES_ARRAY_ARGUMENT template is sufficient ), for the rest, we introduce the version through the COUNTOF_REQUIRES_ARRAY_ARGUMENT template function . Visual Studio again distinguished itself by having its own _countof implementation in the stdlib.h header file .

    The COUNTOF_REQUIRES_ARRAY_ARGUMENT function looks daunting and it’s pretty hard to figure out what it does. If you look closely, you can understand that it takes as its input a single argument an array of elements of the template type Tand the size of N - so in the case of transfer of other types of elements (not arrays) we get a compilation error, which undoubtedly pleases. Peering more closely can be sorted out (with difficulty) that it returns an array of char size N . The question is why do we need all this? This is where the sizeof operator comes in and its unique ability to work at compile time. The call sizeof ( COUNTOF_REQUIRES_ARRAY_ARGUMENT ) determines the size of the array returned by the array of char elements , and since the sizeof (char) == 1 standard , this is the number of elements Nin the source array. Exquisite, beautiful, and completely free.

    forever


    Another small helper macro that I use wherever an infinite loop is needed is forever . It is defined as:

    #if !defined(forever)#define forever for(;;)#else#define STRINGIZE_HELPER(x) #x#define STRINGIZE(x) STRINGIZE_HELPER(x)#define WARNING(desc) message(__FILE__ "(" STRINGIZE(__LINE__) ") : warning: " desc)#pragma WARNING("stdex library - macro 'forever' was previously defined by user; ignoring stdex macro definition")#undef STRINGIZE_HELPER#undef STRINGIZE#undef WARNING#endif

    example syntax for defining an explicit infinite loop:

    unsignedint i = 0;
        forever
        {
            ++i;
        }
    

    This macro is used exclusively for the explicit definition of an infinite loop and is included in the library only for the reasons of “add syntactic sugar”. Hereinafter suppose it is replaced by optional via plug define macro FOREVER . What is remarkable in the above code snippet from the library is the same WARNING macro that generates a warning message in all compilers if the forever macro has already been defined by the user. It uses the familiar standard macro __LINE__ and also the standard __FILE__ , which is converted to a string with the name of the current source file.

    stdex_assert


    To implement assert , a macro stdex_assert is introduced in runtime as:

    #if defined(assert)#ifndef NDEBUG#include<iostream>#define stdex_assert(condition, message) \
    	do { \
    		if (! (condition)) { \
    			std::cerr << "Assertion `" #condition "` failed in " << __FILE__ \
    					  << " line " << __LINE__ << ": " << message << std::endl; \
    			std::terminate(); \
    		} \
    	} while (false)#else#define stdex_assert(condition, message) ((void)0)#endif#endif

    I will not say that I am very proud of this implementation (it will be changed in the future), but here I used an interesting trick to which I would like to draw attention. In order to hide the checks from the scope of the application code, the do {} while (false) construct is used , which is executed, which is obvious, once and does not include the "service" code in the general application code. This technique is quite useful and is used in several places in the library.

    The rest of the implementation is very similar to the standard assert - with a certain macro NDEBUG, which is usually set by compilers in release builds, assert does nothing, otherwise it interrupts program execution with outputting a message to the standard error stream if the assert condition is not followed.

    noexcept


    For functions that do not throw exceptions, the noexcept keyword was introduced in the new standard . It is also quite simple and painlessly possible to implement through a macro:

    #ifdef _STDEX_NATIVE_CPP11_SUPPORT#define stdex_noexcept noexcept#else#define stdex_noexcept throw()#endif

    however, you must understand that according to the standard noexcept can take the value bool , and also be used to determine at compile time that the expression passed to it does not throw exceptions. This functionality cannot be implemented without compiler support, and therefore there is only a “truncated” stdex_noexcept in the library .

    The end of the second chapter. The third chapter will discuss the intricacies of the implementation of nullptr, why it is different for different compilers, as well as how type_traits was advanced and what other bugs in compilers I came across while developing it.

    Thank you for attention.

    Also popular now: