¿Cómo escribí la biblioteca estándar de C ++ 11 o por qué boost es tan aterrador? Capítulo 2

Sí, sí, con este lema me apresuré a la batalla.

Resumen de partes anteriores


Debido a las restricciones en la capacidad de usar compiladores de C ++ 11, y por la falta de alternativa, boost quería escribir su propia implementación de la biblioteca estándar de C ++ 11 sobre la biblioteca de C ++ 98 / C ++ 03 suministrada con el compilador.

Además de los archivos de encabezado estándar, se agregaron type_traits , thread , mutex , chrono , nullptr.h que implementa std :: nullptr_t y core.h donde se agregaron macros relacionadas con la funcionalidad dependiente del compilador, así como expandir la biblioteca estándar.

Enlace a GitHub con el resultado de hoy para impacientes y no lectores:

Los compromisos y las críticas constructivas son bienvenidos

Tabla de contenidos


Introduccion
Capítulo 1. Viam supervadet vadens
Capítulo 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Capítulo 3. Encontrar la implementación nullptr perfecta
Capítulo 4. Magia de plantilla de C ++
.... 4.1 Comenzamos pequeño
.... 4.2 Acerca de cuántos errores milagrosos compila el registro para nosotros
.... 4.3 Punteros y todo-todo-todo
.... 4.4 ¿Qué más se necesita para la biblioteca de plantillas?
Capitulo 5
...

Capítulo 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif


Después de que todo el código fue peinado un poco y dividido por encabezados "estándar" en un stdex de espacio de nombres separado, procedí a completar type_traits , nullptr.h y a lo largo del mismo core.h , que contenía macros para determinar la versión del estándar utilizado por el compilador y admitirlo Nullptr nativo , char16_t , char32_t y static_assert .

En teoría, todo es simple: de acuerdo con el estándar C ++ (cláusula 14.8), el compilador debe definir la macro __cplusplus y debe coincidir con la versión del estándar admitido:

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 

en consecuencia, el código para determinar la disponibilidad de soporte es 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 

imagen De hecho, no todo es tan simple y ahora comienzan las muletas interesantes con un rastrillo.

En primer lugar, no todos, o más bien ninguno, de los compiladores no implementan el siguiente estándar de forma completa e inmediata. Por ejemplo, en Visual Studio 2013, constexpr estuvo ausente durante mucho tiempo, mientras se afirmaba que era compatible con C ++ 11, con la advertencia de que la implementación no estaba completa. Es decir, auto , por favor, static_assert , es igual de fácil (incluso de MS VS anteriores), pero constexpr no lo es. En segundo lugar, no todos los compiladores (y esto es aún más sorprendente) exponen correctamente esta definición y la actualizan de manera oportuna. Inesperadamente, en el mismo compilador, Visual Studio no cambió la versión de __cplusplus define desde las primeras versiones del compilador, aunque se ha declarado por completo el soporte para C ++ 11 (lo cual tampoco es cierto, para lo cual hay rayos separados de descontento, tan pronto como la conversación llega a la funcionalidad específica del "nuevo "11 desarrolladores estándar dicen de inmediato que no hay un preprocesador C99, no hay otras" características "). Y la situación se ve agravada por el hecho de que los compiladores estándar pueden establecer esta definición en diferente de los valores anteriores, si no cumplen totalmente con los estándares declarados. Sería lógico suponer, por ejemplo, tal desarrollo de definiciones para una macro dada (con la introducción de una nueva funcionalidad, aumentar el número oculto detrás de esta definición):

 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 

Pero al mismo tiempo, ninguno de los principales compiladores populares está "desgastado" con esta característica.

Debido a todo esto (no tengo miedo de esta palabra), ahora para cada compilador no estándar tiene que escribir sus propias comprobaciones específicas para averiguar qué estándar de C ++ y en qué medida es compatible. La buena noticia es que necesitamos aprender algunas funciones del compilador para que funcionen correctamente. Primero, ahora agregamos la verificación de versión para Visual Studio a través de la macro _MSC_VER , exclusiva de este compilador. Dado que en mi arsenal de compiladores compatibles también hay C ++ Borland Builder 6.0, cuyos desarrolladores, a su vez, estaban muy interesados ​​en mantener la compatibilidad con Visual Studio (incluidas sus "características" y errores), de repente también aparece esta macro. Para los compiladores compatibles con clang, hay una macro no estándar __has_feature ( feature_name ) , que le permite averiguar si el compilador admite esta o aquella funcionalidad. Como resultado, el código se hincha para:

 #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 

¿Quieres llegar a más compiladores? Agregamos controles para Codegear C ++ Builder, que es el heredero de Borland (en sus peores manifestaciones, pero más sobre eso más adelante):

 #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 

También vale la pena señalar que, dado que Visual Studio ya ha implementado el soporte nullptr de la versión del compilador _MSC_VER 1600 , así como los tipos incorporados char16_t y char32_t , debemos manejar esto correctamente. Se agregaron algunos controles más:

 #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 

Al mismo tiempo, verificaremos el soporte de C ++ 98, ya que para los compiladores sin él no habrá algunos archivos de encabezado de la biblioteca estándar, y no podemos verificar la ausencia de ellos utilizando el compilador.

Opción completa
 #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 


Y ahora las configuraciones voluminosas de boost están comenzando a aparecer en mi memoria en la que muchos desarrolladores trabajadores escribieron todas estas macros dependientes del compilador e hicieron un mapa de lo que es compatible y lo que no es por un compilador específico de una versión específica, de lo cual personalmente me siento incómodo, No quiero mirarlo ni tocarlo nunca más. Pero la buena noticia es que puedes detenerte allí. Al menos esto es suficiente para mí para admitir los compiladores más populares, pero si encuentra una inexactitud o desea agregar otro compilador, estaré muy feliz de aceptar la solicitud de extracción.

Un gran logro en comparación con el impulso, creo que fue posible mantener la difusión de las macros dependientes del compilador en todo el código, lo que hace que el código sea más limpio y fácil de entender, y tampoco acumule docenas de archivos de configuración para cada sistema operativo y para cada compilador. Hablaremos de las desventajas de este enfoque un poco más adelante.

En esta etapa, ya podemos comenzar a conectar la funcionalidad faltante de los 11 estándares, y lo primero que presentamos es static_assert .

static_assert


Definimos la estructura StaticAssertion , que tomará un valor booleano como un parámetro de plantilla: existirá nuestra condición, si no se cumple (la expresión es falsa ), se producirá un error al compilar una plantilla no especializada. Y otra estructura ficticia para recibir sizeof ( StaticAssertion ) .

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

y más macro magia

 #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 

uso:

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

Una diferencia importante entre mi implementación y la estándar es que no hay sobrecarga de esta palabra clave sin decirle al usuario. Esto se debe al hecho de que en C ++ es imposible definir varias definiciones con diferentes números de argumentos pero un solo nombre, y una implementación sin mensaje es mucho menos útil que la opción seleccionada. Esta característica lleva al hecho de que, en esencia, STATIC_ASSERT en mi implementación es la versión agregada ya en C ++ 11.
Echemos un vistazo a lo que sucedió. Como resultado de verificar las versiones de __cplusplus y las macros de compilación no estándar, tenemos suficiente información sobre el soporte de C ++ 11 (y, por lo tanto, static_assert ), expresada por la definición _STDEX_NATIVE_CPP11_SUPPORT. Por lo tanto, si esta macro está definida, simplemente podemos usar el static_assert estándar:

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

Tenga en cuenta que el segundo parámetro de la macro STATIC_ASSERT no es una cadena literal en absoluto, y por lo tanto, utilizando el operador de preprocesador # convertiremos el parámetro del mensaje en una cadena para su transmisión al static_assert estándar.
Si no tenemos soporte del compilador, procedemos a nuestra implementación. Para comenzar, declararemos macros auxiliares para "pegar" cadenas (el operador del preprocesador ## es el único responsable de esto).

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

Específicamente, no utilicé simplemente #define CONCATENATE ( arg1 , arg2 ) arg1 ## arg2 para poder pasar el resultado de la misma macro CONCATENATE como argumento a arg1 y arg2 .
A continuación, declaramos una estructura con el hermoso nombre __static_assertion_at_line_ {número de línea} (la macro __LINE__ también está definida por el estándar y debe expandirse al número de línea en el que se llamó), y dentro de esta estructura agregamos un campo de nuestro tipo StaticAssertion con el nombre STATIC_ASSERTION_FAILED_AT_LINE_ {line number} _WITH__ { mensajes de error de la macro que llama}.

 #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__) 

Con el parámetro de plantilla en StaticAssertion, pasamos una expresión que se verifica en STATIC_ASSERT , lo que lleva a bool . Finalmente, para evitar crear variables locales y verificar la condición del usuario sin sobrecarga, se declara un alias para el tipo StaticAssertionTest <sizeof ({nombre de la estructura declarada anteriormente}) con el nombre __static_assertion_test_at_line_ {número de línea}.

Toda la belleza de la nomenclatura es necesaria solo para aclarar a partir de un error de compilación que este es un resultado de afirmación, y no solo un error, sino también para mostrar un mensaje de error que se configuró para esta afirmación. El truco sizeof es necesario para forzar al compilador a crear una instancia de la clase de plantilla StaticAssertion , que está dentro de la estructura recién declarada, y así verificar la condición pasada para afirmar.

STATIC_ASSERT resultados
CCG:
30: 103: error: el campo 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' tiene un tipo incompleto 'stdex :: detail :: StaticAssertion <false>'
25:36: nota: en la definición de macro 'CONCATENATE2'
23:36: nota: en expansión de la macro 'CONCATENATE1'
30:67: nota: en expansión de la macro 'CONCATENAR'
24:36: nota: en expansión de la macro 'CONCATENATE2'
23:36: nota: en expansión de la macro 'CONCATENATE1'
30:79: nota: en expansión de la macro 'CONCATENAR'
24:36: nota: en expansión de la macro 'CONCATENATE2'
23:36: nota: en expansión de la macro 'CONCATENATE1'
30:91: nota: en expansión de la macro 'CONCATENAR'
36: 3: nota: en la expansión de la macro 'STATIC_ASSERT'

Borland C ++ Builder:
[Error de C ++] stdex_test.cpp (36): E2450 Estructura no definida 'stdex :: detail :: StaticAssertion <0>'
[Error de C ++] stdex_test.cpp (36): E2449 El tamaño de 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' es desconocido o cero
[Error de C ++] stdex_test.cpp (36): E2450 Estructura no definida 'stdex :: detail :: StaticAssertion <0>'

Visual Studio:
Error c2079


El segundo "truco" que quería tener, aunque faltaba en el estándar, es contar : contar el número de elementos en la matriz. A las hermanas les gusta declarar esta macro a través de sizeof (arr) / sizeof (arr [0]), pero iremos más allá.

cuenta de


 #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 

Para los compiladores con soporte constexpr , declararemos una versión constexpr de esta plantilla (que no es absolutamente necesaria, para todos los estándares, la implementación a través de la plantilla COUNTOF_REQUIRES_ARRAY_ARGUMENT es suficiente ), por lo demás presentamos la versión a través de la función de plantilla COUNTOF_REQUIRES_ARRAY_ARGUMENT . Visual Studio aquí nuevamente se distingue por la presencia de su propia implementación de _countof en el archivo de encabezado stdlib.h .

La función COUNTOF_REQUIRES_ARRAY_ARGUMENT parece intimidante y descubrir lo que hace es bastante complicado. Si observa detenidamente, puede comprender que toma una matriz de elementos de tipo de plantilla T y tamaño N como único argumento; por lo tanto, en el caso de transferir otros tipos de elementos (no matrices), obtenemos un error de compilación, que indudablemente agrada. Echando un vistazo más de cerca, puede descubrir (con dificultad) que devuelve una serie de elementos de tamaño N. La pregunta es, ¿por qué necesitamos todo esto? Aquí es donde entra en juego el operador sizeof y su capacidad única de trabajar en tiempo de compilación. El tamaño de la llamada ( COUNTOF_REQUIRES_ARRAY_ARGUMENT ) determina el tamaño del conjunto de elementos char devueltos por la función, y dado que el tamaño estándar de (char) == 1, este es el número de N elementos en el conjunto original. Elegante, hermoso y completamente gratis.

por siempre


Otra pequeña macro auxiliar que uso siempre que se necesita un bucle infinito es para siempre . Se define de la siguiente manera:

 #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 

Ejemplo de sintaxis para definir un bucle infinito explícito:

  unsigned int i = 0; forever { ++i; } 

Esta macro se usa únicamente para definir explícitamente un bucle infinito y se incluye en la biblioteca solo por razones de "agregar azúcar sintáctico". En el futuro, propongo reemplazarlo con opcionalmente a través de definir la macro del plugin FOREVER . Lo que es notable en el fragmento de código anterior de la biblioteca es la misma macro WARNING que genera un mensaje de advertencia en todos los compiladores si el usuario ya ha definido la macro forever . Utiliza la familiar macro estándar __LINE__ y la macro estándar __FILE__ , que se convierte en una cadena con el nombre del archivo fuente actual.

stdex_assert


Para implementar la aserción en tiempo de ejecución, la macro stdex_assert se presenta como:

 #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 

No diré que estoy muy orgulloso de esta implementación (se cambiará en el futuro), pero aquí se ha utilizado una técnica interesante a la que me gustaría llamar la atención. Para ocultar las comprobaciones del alcance del código de la aplicación, se utiliza la construcción do {} while (false) , que se ejecutará, lo que es obvio una vez y al mismo tiempo no introducirá el código de "servicio" en el código general de la aplicación. Esta técnica es bastante útil y se usa en varios lugares de la biblioteca.

De lo contrario, la implementación es muy similar a la afirmación estándar: con una cierta macro NDEBUG , que los compiladores generalmente configuran en versiones de lanzamiento, afirma no hace nada, de lo contrario, interrumpe la ejecución del programa con la salida del mensaje a la secuencia de error estándar si no se cumple la condición de afirmación.

no excepto


Para las funciones que no arrojan excepciones, la palabra clave noexcept se ha introducido en el nuevo estándar. También es bastante simple e indoloro posible implementarlo a través de la macro:

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

sin embargo, es necesario comprender que, según el estándar, noexcept puede tomar el valor bool , y también puede usarse para determinar en tiempo de compilación que la expresión que se le pasa no arroja una excepción. Esta funcionalidad no se puede implementar sin el soporte del compilador y, por lo tanto, solo hay un stdex_noexcept "despojado" en la biblioteca.

El final del segundo capítulo. El tercer capítulo hablará sobre las complejidades de la implementación nullptr, por qué es diferente para diferentes compiladores, así como el desarrollo de type_traits, y qué otros errores en los compiladores me encontré durante su desarrollo.

Gracias por su atencion

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


All Articles