📜 ⬆️ ⬇️

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

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

Link to GitHub with the result today for the 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 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 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 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 “ Native ” nullptr , 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 

image In 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 (even 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, the same Visual Studio compiler did not change the version of the __cplusplus define already from the very first versions of the compiler, although full support for C ++ 11 has long been declared (which is also not true, for which some rays of dissatisfaction are coming to them - as soon as the conversation turns to specific functionality of the “new "11 standard developers immediately say that there is no C99 preprocessor, there are still 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 won't be afraid of this word) the mess now for each non-standard compiler you have to write your 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 of the functions of the compiler to work correctly. First, now we 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), then there is also suddenly this macro. 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 there will not be some standard library header files, and we can’t verify 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 a lot of hardworking developers have written out all these compiler-dependent macros begin to appear in memory and of them compiled a map of what is supported and what is not a specific compiler for a particular version, which personally makes me uncomfortable, 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-dependent 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> struct StaticAssertion; template <> struct StaticAssertion<true> { }; // StaticAssertion<true> template<int i> struct StaticAssertionTest { }; // 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 an implementation without a message is much less useful than the chosen 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 convert the message parameter to a string to be transferred to the standard static_assert .
If we don’t have support from the compiler, then we’ll proceed to our implementation. To begin with, we will declare auxiliary macros for “gluing” strings (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 didn’t specifically 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 into 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 a 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 a compilation error that this is the result of assert, and not just an error, as well as to display an error message that was given for this assert. The sizeof trick is needed to force the compiler to instantiate the generic StaticAssertion class 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_since you work


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 <class T, std::size_t N> constexpr std::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 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 take a closer look, you can understand that it takes as its input a single argument an array of elements of the template type T and size N - so in the case of transferring other types of elements (not arrays) we get a compilation error, which undoubtedly pleases. Looking more closely, you can figure out (with difficulty) that it returns an array of char elements of size N. The question is why do we need all this? This is where the sizeof operator comes into play 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 N elements in 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:

  unsigned int 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”. In the future, I intend to replace it with an option via define. What is noteworthy in the above code snippet from the library is the very 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.

Otherwise, the implementation is very similar to the standard assert - with a certain NDEBUG macro, which is usually set by compilers in release assemblies, assert does nothing, otherwise it interrupts program execution with outputting the 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.

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


All Articles