
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
IntroduccionCapítulo 1. Viam supervadet vadensCapítulo 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endifCapítulo 3. Encontrar la implementación nullptr perfectaCapí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
en consecuencia, el código para determinar la disponibilidad de soporte es trivial:
#if (__cplusplus >= 201103L)

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
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
¿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
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
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
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> { };
y más macro magia
#ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) #else
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 resultadosCCG:
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