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

Seguimos la aventura.

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.

Se implementaron Static_assert , noexcept , countof , y también, después de considerar todas las características de compilador y definiciones no estándar, apareció información sobre la funcionalidad que es compatible con el compilador actual. Se incluye su propia implementación de nullptr , que se selecciona en la etapa de compilación.

Ha llegado el momento de type_traits y toda esta "plantilla mágica especial". En la primera parte, examinamos mi implementación de las plantillas más simples de la biblioteca estándar, pero ahora profundizaremos en las plantillas.

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

Los compromisos y las críticas constructivas son bienvenidos

Continuación de la inmersión en el mundo de la "plantilla mágica" C ++.

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 4. Plantilla "mágica" C ++. Continuación


4.2 Acerca de cuántos errores milagrosos compila el registro


En la primera parte de este capítulo, se introdujeron las plantillas básicas de type_traits , pero faltaban algunas más para el conjunto completo.

Por ejemplo, simplemente se necesitaban las plantillas is_integral y is_floating_point , que en realidad se definen de manera muy trivial, a través de la especialización de plantilla para cada tipo incorporado. La pregunta aquí solo surgió con los tipos "grandes" de largo largo . El hecho es que este tipo como incorporado aparece en el estándar del lenguaje C ++ solo a partir de la versión 11. Y sería lógico suponer que todo se reduce a verificar la versión del estándar C ++ (que de todos modos es difícil de determinar ), pero no estaba allí.

imagen Porque, desde 1999, existe el estándar de lenguaje C99 C en el que los tipos long long int y unsigned long long int ya han estado presentes (¡desde 1999!), Y dado que el lenguaje C ++ buscó mantener la compatibilidad con versiones anteriores de C puro, muchos compiladores (que generalmente se mezclaron C / C ++) simplemente lo añadieron como un tipo fundamental incluso antes de que se lanzara el estándar C ++ 03. Es decir, la situación era que el tipo incorporado es de hecho (de C), pero no se describe en el estándar C ++ y no debería estar allí. Y eso agrega un poco más de confusión a la implementación de la biblioteca estándar. Pero veamos el código:

namespace detail { template <class> struct _is_floating_point : public false_type {}; template<> struct _is_floating_point<float> : public true_type {}; template<> struct _is_floating_point<double> : public true_type {}; template<> struct _is_floating_point<long double> : public true_type {}; } template <class _Tp> struct is_floating_point : public detail::_is_floating_point<typename remove_cv<_Tp>::type> { }; 

Todo está claro con el código anterior: especializamos la plantilla para los tipos de punto flotante necesarios y, después de "borrar" los modificadores de tipo, decimos "sí" o "no" al tipo que se nos pasa. Los siguientes en la línea son los tipos enteros:

 namespace detail { template <class> struct _is_integral_impl : public false_type {}; template<> struct _is_integral_impl<bool> : public true_type {}; template<> struct _is_integral_impl<char> : public true_type {}; template<> struct _is_integral_impl<wchar_t> : public true_type {}; template<> struct _is_integral_impl<unsigned char> : public true_type {}; template<> struct _is_integral_impl<unsigned short int> : public true_type {}; template<> struct _is_integral_impl<unsigned int> : public true_type {}; template<> struct _is_integral_impl<unsigned long int> : public true_type {}; #ifdef LLONG_MAX template<> struct _is_integral_impl<unsigned long long int> : public true_type {}; #endif template<> struct _is_integral_impl<signed char> : public true_type {}; template<> struct _is_integral_impl<short int> : public true_type {}; template<> struct _is_integral_impl<int> : public true_type {}; template<> struct _is_integral_impl<long int> : public true_type {}; #ifdef LLONG_MAX template<> struct _is_integral_impl<long long int> : public true_type {}; #endif template <class _Tp> struct _is_integral : public _is_integral_impl<_Tp> {}; template<> struct _is_integral<char16_t> : public true_type {}; template<> struct _is_integral<char32_t> : public true_type {}; template<> struct _is_integral<int64_t> : public true_type {}; template<> struct _is_integral<uint64_t> : public true_type {}; } template <class _Tp> struct is_integral : public detail::_is_integral<typename remove_cv<_Tp>::type> { }; 

Aquí debes detenerte un poco y pensar. Para tipos enteros "antiguos" como int , bool , etc. hacemos las mismas especializaciones que con is_floating_point . Para los "nuevos" tipos long long int y su contraparte sin signo, definimos sobrecargas solo si hay una definición LLONG_MAX, que se definió en C ++ 11 (como el primer estándar C ++ que es compatible con C99), y debe definirse en el archivo de encabezado de ascensos como máximo un gran número que cabe en un objeto de tipo long long int . Climits también tiene algunas definiciones de macro más (para el número más pequeño posible y equivalentes sin signo), pero decidí usar esta macro, que no es importante. Lo importante es que, a diferencia de boost, en esta implementación los tipos "grandes" de C no se definirán como constantes enteras, aunque estén (posiblemente) presentes en el compilador. Lo que es más importante son los tipos char16_t y char32_t , que también se introdujeron en C ++ 11, pero ya no se entregaron en C99 (aparecieron ya simultáneamente con C ++ en el estándar C11) y, por lo tanto, en los estándares antiguos, su definición puede solo a través de un alias de tipo (por ejemplo typedef short char16_t , pero más sobre eso más adelante). Si es así, para que la especialización de plantilla maneje correctamente las situaciones en que estos tipos están separados (integrados) y cuando se definen a través de typedef , se necesita una capa más de detalles de especialización de plantilla :: _ is_integral .

Un hecho interesante es que en algunos compiladores antiguos estos tipos "grandes" C-shy no son constantes integrales . Lo que se puede entender e incluso perdonar, ya que estos tipos no son estándar para C ++ hasta 11 estándares, y en general no deberían estar allí. Pero lo que es difícil de entender es que estos tipos en el último compilador de C ++ de la creatividad Embarcadero (Embarcadero C ++ Builder), que C ++ 11 supuestamente admite, todavía no son constantes en sus ensambles de 32 bits (como hace 20 años entonces era Borland todavía cierto). Aparentemente debido a esto, incluyendo, la mayoría de la biblioteca estándar de C ++ 11 falta en estos ensambles de 32 bits (#include ratio? Chrono? Will cost). Embarcadero parece haber decidido forzar la era de 64 bits con el lema: “¿Quieres C ++ 11 o un estándar más nuevo? ¡Cree un programa de 64 bits (y solo un sonido metálico, nuestro compilador no puede)!

Una vez finalizados los procedimientos con los tipos fundamentales de lenguaje, presentamos algunos patrones más simples:

Patrones simples
 template <bool, class _Tp = detail::void_type> struct enable_if { }; template <class _Tp> struct enable_if<true, _Tp> { typedef _Tp type; }; template<class, class> struct is_same : public false_type { }; template<class _Tp> struct is_same<_Tp, _Tp> : public true_type//specialization { }; template <class _Tp> struct is_const : public false_type { }; template <class _Tp> struct is_const<const _Tp> : public true_type { }; template <class _Tp> struct is_const<const volatile _Tp> : public true_type { }; /// is_volatile template<class> struct is_volatile : public false_type { }; template<class _Tp> struct is_volatile<volatile _Tp> : public true_type { }; template<class _Tp> struct is_volatile<const volatile _Tp> : public true_type { }; 


Solo el hecho de que las plantillas se especializan para todos los modificadores del tipo ( volátil y constante, por ejemplo) es notable aquí, porque algunos compiladores tienden a "perder" uno de los modificadores al expandir la plantilla.

Por separado, destaco la implementación de is_signed y is_unsigned :

 namespace detail { template<bool> struct _sign_unsign_chooser; template<class _Tp> struct _signed_comparer { static const bool value = _Tp(-1) < _Tp(0); }; template<class _Tp> struct _unsigned_comparer { static const bool value = _Tp(0) < _Tp(-1); }; template<bool Val> struct _cat_base : integral_constant<bool, Val> { // base class for type predicates }; template<> struct _sign_unsign_chooser<true>//integral { template<class _Tp> struct _signed : public _cat_base<_signed_comparer<typename remove_cv<_Tp>::type>::value> { }; template<class _Tp> struct _unsigned : public _cat_base<_unsigned_comparer<typename remove_cv<_Tp>::type>::value> { }; }; template<> struct _sign_unsign_chooser<false>//floating point { template<class _Tp> struct _signed : public is_floating_point<_Tp> { }; template<class _Tp> struct _unsigned : public false_type { }; }; } template<class T> struct is_signed { // determine whether T is a signed type static const bool value = detail::_sign_unsign_chooser<is_integral<T>::value>::template _signed<T>::value; typedef const bool value_type; typedef integral_constant<bool, is_signed::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; template<class T> struct is_unsigned { // determine whether T is an unsigned type static const bool value = detail::_sign_unsign_chooser<is_integral<T>::value>::template _unsigned<T>::value; typedef const bool value_type; typedef integral_constant<bool, is_unsigned::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; 

Al implementar esta parte, entré en una batalla desigual con Borland C ++ Builder 6.0, que no quería hacer que estas dos plantillas heredaran de integral_constant , lo que eventualmente resultó en docenas de errores internos del compilador "imitando" el comportamiento integral_constant para estas plantillas. Aquí, tal vez, valga la pena seguir luchando y llegar a algún tipo de derivación complicada del tipo is_ * un * firmado: integral_constant a través de plantillas, pero hasta ahora he pospuesto esta tarea como no una prioridad. Lo interesante de la sección de código anterior es cómo en el momento de la compilación se determina que el tipo no está firmado / firmado. Para empezar, todos los tipos no enteros están marcados y para ellos la plantilla va a una rama especializada separada _sign_unsign_chooser con el argumento de plantilla false , que a su vez siempre devuelve value == false para cualquier tipo, excepto los tipos de coma flotante estándar (siempre están firmados por razones obvias, entonces _signed :: value será verdadero ). Para los tipos enteros, simples, pero bastante entretenidos, se realizan verificaciones. Aquí usamos el hecho de que para los tipos enteros sin signo, cuando el número disminuye y luego pasa a un mínimo (obviamente 0), se produce un desbordamiento y el número adquiere su máximo valor posible.

Este hecho es bien conocido, así como el de ese tipo icónica de desbordamiento es un comportamiento indefinido y para ello es necesario seguir (de acuerdo a la norma, no se puede reducir la variable int menos de INT_MIN y la esperanza de que, como resultado de desbordamiento llegar INT_MAX, en lugar de 42 o disco duro formateado )

Escribimos _Tp (-1) <_Tp (0) para verificar el tipo de "signo" utilizando este hecho, luego para los tipos sin signo -1 "se transforma" a través del desbordamiento al número máximo de este tipo, mientras que para los tipos con signo dicha comparación se realizará sin desbordamiento, y -1 se comparará con 0.

Y el último para hoy, pero lejos del último "truco" en mi biblioteca es la implementación de alineación_de :

 namespace detail { template <class _Tp> struct _alignment_of_trick { char c; _Tp t; _alignment_of_trick(); }; template <unsigned A, unsigned S> struct _alignment_logic_helper { static const std::size_t value = A < S ? A : S; }; template <unsigned A> struct _alignment_logic_helper<A, 0> { static const std::size_t value = A; }; template <unsigned S> struct _alignment_logic_helper<0, S> { static const std::size_t value = S; }; template< class _Tp > struct _alignment_of_impl { #if _MSC_VER > 1400 // // With MSVC both the build in __alignof operator // and following logic gets things wrong from time to time // Using a combination of the two seems to make the most of a bad job: // static const std::size_t value = (_alignment_logic_helper< sizeof(_alignment_of_trick<_Tp>) - sizeof(_Tp), __alignof(_Tp) >::value); #else static const std::size_t value = (_alignment_logic_helper< sizeof(_alignment_of_trick<_Tp>) - sizeof(_Tp), sizeof(_Tp) >::value); #endif typedef integral_constant<std::size_t, std::size_t(_alignment_of_impl::value)> type; private: typedef intern::type_traits_asserts check; typedef typename check::alignment_of_type_can_not_be_zero_assert< _alignment_of_impl::value != 0 >:: alignment_of_type_can_not_be_zero_assert_failed check1; // if you are there means aligment of type passed can not be calculated or compiler can not handle this situation (sorry, nothing can be done there) }; // borland compilers seem to be unable to handle long double correctly, so this will do the trick: struct _long_double_wrapper{ long double value; }; } template <class _Tp> struct alignment_of: public detail::_alignment_of_impl<_Tp>::type {}; template <class _Tp> struct alignment_of<_Tp&>: public alignment_of<_Tp*> {}; template<> struct alignment_of<long double>: public alignment_of<detail::_long_double_wrapper> {}; 

Microsoft nuevamente se destacó aquí con su Visual Studio, que incluso teniendo una macro incorporada no estándar __alignof incorporada , aún produce resultados incorrectos al usarlo.

Explicación del impulso
Los usuarios de Visual C ++ deben tener en cuenta que MSVC tiene diferentes definiciones de "alineación". Por ejemplo, considere el siguiente código:

 typedef long long align_t; assert(boost::alignment_of<align_t>::value % 8 == 0); align_t a; assert(((std::uintptr_t)&a % 8) == 0); char c = 0; align_t a1; assert(((std::uintptr_t)&a1 % 8) == 0); 

En este código, aunque boost :: alinear_de <align_t> informa que align_t tiene una alineación de 8 bytes, la afirmación final fallará para una compilación de 32 bits porque a1 no está alineado en un límite de 8 bytes. Tenga en cuenta que si hubiéramos utilizado el MSVC intrínseco __alignof en lugar de boost :: alineación_of, aún obtendríamos el mismo resultado. De hecho, los requisitos de alineación de MSVC (y las promesas) solo se aplican realmente al almacenamiento dinámico, y no a la pila.


Permítame recordarle lo que debe hacer la plantilla std ::ignment_of: devolver un valor que represente los requisitos para la colocación de un elemento de este tipo en la memoria. Un poco de distracción, entonces un elemento de cada tipo tiene algún tipo de asignación de memoria, y si es continuo para una matriz de elementos, entonces, por ejemplo, las clases pueden tener "agujeros" entre los elementos miembros de la clase ( sizeof class struct { char a;} probablemente no sea igual a 1, aunque hay 1 byte de todo dentro, porque el compilador lo alineará a 1 + 3 bytes durante el proceso de optimización).

Ahora veamos el código nuevamente. Declaramos la estructura _alignment_of_trick , en la que colocamos un elemento del tipo que se verifica con una "sangría" en la memoria de 1 byte. Y verifique la alineación simplemente restando el tamaño del tipo que se verifica del tamaño de la estructura resultante. Es decir, si el compilador decide "pegar" un espacio vacío entre el elemento del tipo que se está verificando y el carácter anterior, entonces obtenemos el valor de alineación de tipo en la estructura.

También aquí la aserción estática se encuentra primero como un tipo. Se declaran como:

 namespace intern { // since we have no static_assert in pre-C++11 we just compile-time assert this way: struct type_traits_asserts { template<bool> struct make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert; template<bool> struct make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert; template<bool> struct not_allowed_arithmetic_type_assert; template<bool> struct alignment_of_type_can_not_be_zero_assert; }; template<> struct type_traits_asserts::make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert<true> { typedef bool make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert_failed; }; template<> struct type_traits_asserts::make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert<true> { typedef bool make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert_failed; }; template<> struct type_traits_asserts::not_allowed_arithmetic_type_assert<true> { typedef bool not_allowed_arithmetic_type_assert_failed; }; template<> struct type_traits_asserts::alignment_of_type_can_not_be_zero_assert<true> { typedef bool alignment_of_type_can_not_be_zero_assert_failed; }; } 

De hecho, estas plantillas especializadas son necesarias para reemplazar el static_assert de C ++ 11, que se encuentra dentro de la definición de clase. Tal aserción carácter más ligero y propietaria de la static_assert general de ejecución del capítulo 2 , y no dejar que tirar de un archivo de cabecera en type_traits core.h.

imagen ¿Muchos patrones? Habrá más! Nos detendremos en esto por ahora, a medida que continúe la fascinante historia sobre la combinación de la programación de plantillas con la tecnología SFINAE, así como por qué tuve que escribir un pequeño generador de código.

Gracias por su atencion

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


All Articles