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

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 las partes anteriores de este capítulo, examinamos mi implementación de las plantillas básicas de la biblioteca estándar, y en esta parte hablaremos sobre la combinación de la técnica SFINAE con plantillas y un poco sobre la generación de código.

Enlace a GitHub con el resultado de hoy para impacientes y no lectores:
Los compromisos y las críticas constructivas son bienvenidos
Más plantillas de C ++ en cat.

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.3 Punteros y todo-todo-todo


En esta etapa, solo pude obtener información sobre si el tipo es una matriz para std :: is_array y fue posible iniciar plantillas para punteros. La implementación también fue trivial, pero no sin supuestos.

// is_array template<class> struct is_array : public false_type { }; template<class _Tp, std::size_t _Size> struct is_array<_Tp[_Size]> : public true_type { }; /*template<class _Tp> struct is_array<_Tp[]>: public true_type { }; */ 

Una especialización de plantilla simple para matrices de una longitud dada "captura" todos los tipos de matrices, sin embargo, el problema surge con el tipo incompleto T [] (una matriz sin especificar la longitud). El hecho es que este tipo no está definido por algunos compiladores (C ++ Builder) cuando se especializa una plantilla, y todavía no he encontrado una solución universal aquí.

Después de que la biblioteca fue "enseñada" a definir tipos incorporados, alineación en la memoria de tipos, trabajar con modificadores de tipos y otras cosas básicas a través de plantillas en tiempo de compilación, llegó el momento de los punteros y las referencias.

imagen En C ++, se pueden distinguir dos grupos de punteros: punteros a miembros de la clase y punteros a otros objetos. ¿Por qué es importante esta separación para una mayor implementación de la biblioteca estándar? El hecho es que los punteros a los miembros de la clase tienen una diferencia significativa de otros punteros por la presencia de esto , es decir. puntero a un objeto de esta clase. Y, de manera estándar, los punteros a un miembro de la clase tienen una sintaxis separada para definir, son un tipo separado y no pueden representarse a través de un puntero regular. En la práctica, esto se traduce en el hecho de que el tamaño de un puntero a un miembro de la clase suele ser mayor que el tamaño de un puntero regular (que == sizeof (void *) ), porque Para implementar funciones de miembro virtuales de la clase, así como para almacenar este puntero, los compiladores generalmente implementan punteros a un miembro de clase como una estructura (lea sobre las funciones y la estructura virtuales). La forma de presentar punteros a los miembros de la clase se deja, según el estándar, a discreción del compilador, pero recordaremos esta diferencia de tamaño y presentación cuando consideremos más código.

Para definir un puntero regular a un objeto, escribiremos una plantilla simple is_pointer , así como una plantilla is_lvalue_reference para referencias de objeto ( dejamos de lado la referencia is_rvalue_reference, porque hasta el undécimo estándar no había operador && , así como toda la semántica de movimiento):

 namespace detail { template<class> struct _is_pointer_helper : public false_type { }; template<class _Tp> struct _is_pointer_helper<_Tp*> : public true_type { }; } // is_pointer template<class _Tp> struct is_pointer : public detail::_is_pointer_helper<typename remove_cv<_Tp>::type>::type { }; // is_lvalue_reference template<class> struct is_lvalue_reference : public false_type { }; template<class _Tp> struct is_lvalue_reference<_Tp&> : public true_type { }; 

Ya no hay nada fundamentalmente nuevo aquí, todo lo mismo se hizo en las partes anteriores de este capítulo. Continuemos definiendo punteros a objetos; ahora veamos punteros a funciones.
Es importante comprender que una función y una función miembro de una clase son entidades completamente diferentes según el estándar:

  • El primer puntero será normal (un puntero a un objeto), el segundo tendrá un puntero a un miembro de la clase.

 void (*func_ptr)(int); //  'func_ptr'    'void func(int){}' void (ClassType::*mem_func_ptr)(int); //  'mem_func_ptr'  -  'ClassType'  'void ClassType::func(int){}' 

  • Puede crear un enlace al primero (enlace de objeto), pero no puede crear un segundo enlace.

 void (&func_ref)(int); //  'func_ref'    'void func(int){}' //-------------------- //   -     
Aquí solo mencionaré un poco sobre la generación de código. Como antes de C ++ 11 no había plantillas con un número variable de parámetros, todas las plantillas donde podría haber un número diferente de parámetros se determinaron mediante la especialización de la plantilla principal con cualquier gran número de parámetros en la entrada y su inicialización por parámetros ficticios predeterminados. Lo mismo se aplica a las sobrecargas de funciones, como Tampoco hubo macros con un número variable de parámetros. Dado que escribir 60-70 líneas del mismo tipo de especializaciones de plantillas con sus manos, la sobrecarga de funciones es una tarea bastante aburrida e inútil, y también está cargada de la posibilidad de cometer un error. Escribí un generador simple de código para plantillas y sobrecargas de funciones para estos fines. Decidí limitarme a definir funciones a 24 parámetros y esto parece bastante engorroso en el código, pero simple y claro:

 namespace detail { template <class R> struct _is_function_ptr_helper : false_type {}; template <class R > struct _is_function_ptr_helper<R(*)()> : true_type {}; template <class R > struct _is_function_ptr_helper<R(*)(...)> : true_type {}; template <class R, class T0> struct _is_function_ptr_helper<R(*)(T0)> : true_type {}; template <class R, class T0> struct _is_function_ptr_helper<R(*)(T0 ...)> : true_type {}; 

...
  template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24> struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24)> : true_type {}; template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24> struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 ...)> : true_type {}; } 

Definimos los tipos que nos son familiares del capítulo anterior para la técnica SFINAE:

 namespace detail { // SFINAE magic typedef char _yes_type; struct _no_type { char padding[8]; }; } 

Algunas macros más por conveniencia
 namespace detail { #define _IS_MEM_FUN_PTR_CLR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const volatile); #ifdef _STDEX_CDECL _no_type _STDEX_CDECL _is_mem_function_ptr(...); #define _IS_MEM_FUN_CDECL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const volatile); #define _IS_MEM_FUN_STDCALL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const volatile); #define _IS_MEM_FUN_FASTCALL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const volatile); #else _no_type _is_mem_function_ptr(...); #define _IS_MEM_FUN_CDECL_PTR #define _IS_MEM_FUN_STDCALL_PTR #define _IS_MEM_FUN_FASTCALL_PTR #endif #define _IS_MEM_FUN_PTR \ _IS_MEM_FUN_PTR_CLR \ _IS_MEM_FUN_CDECL_PTR \ _IS_MEM_FUN_STDCALL_PTR \ _IS_MEM_FUN_FASTCALL_PTR } 


Las macros se definen para que sea relativamente conveniente redefinir TIPOS y ARGS las defina como una lista de tipos y parámetros, luego sustituya la macro _IS_MEM_FUN_PTR para generar definiciones para todos los tipos de funciones posibles por parte del preprocesador. También vale la pena prestar atención al hecho de que para los compiladores de Microsoft, los acuerdos de llamadas ( __fastcall , __stdcall y __cdecl ) también son importantes , porque con diferentes convenciones, las funciones serán diferentes, aunque tengan el mismo conjunto de argumentos y valor de retorno. Como resultado, todo este grandioso diseño macro se usa de manera bastante compacta:

 namespace detail { #define TYPES #define ARGS _IS_MEM_FUN_PTR #undef TYPES #undef ARGS #define TYPES , class T0 #define ARGS T0 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS #define TYPES , class T0, class T1 #define ARGS T0, T1 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS 

...
  #define TYPES , class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24 #define ARGS T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS //      define  : #undef _IS_MEM_FUN_PTR #undef _IS_MEM_FUN_PTR_CLR #undef _IS_MEM_FUN_CDECL_PTR #undef _IS_MEM_FUN_STDCALL_PTR #undef _IS_MEM_FUN_FASTCALL_PTR } 

Y ahora por lo que estaba escrito:

 namespace detail { template <class _Tp, bool _IsRef> struct _is_mem_function_ptr_impl { static _Tp *p; static const bool value = (sizeof(_is_mem_function_ptr(_is_mem_function_ptr_impl::p)) == sizeof(_yes_type)); typedef typename integral_constant<bool, _is_mem_function_ptr_impl::value == bool(true)>::type type; }; template <class _Tp> struct _is_mem_function_ptr_impl<_Tp, true>: public false_type {}; template <class _Tp> struct _is_mem_function_ptr_helper: public _is_mem_function_ptr_impl<_Tp, is_reference<_Tp>::value>::type {}; template <class _Tp, bool _IsMemberFunctionPtr> struct _is_function_chooser_impl : public false_type { }; template <class _Tp> struct _is_function_chooser_impl<_Tp, false> : public _is_function_ptr_helper<_Tp*> { }; template<class _Tp, bool _IsRef = true> struct _is_function_chooser : public false_type { }; template <class _Tp> struct _is_function_chooser<_Tp, false> { static const bool value = _is_function_chooser_impl<_Tp, _is_mem_function_ptr_helper<_Tp>::value>::value; }; } 

Para verificar si un tipo es una función miembro de una clase, primero se verifica para ver si el tipo es una referencia. Luego se crea un puntero de este tipo y se sustituye en la función de sonda. Utilizando la técnica SFINAE, el compilador selecciona la sobrecarga necesaria de las funciones de la sonda para dicho puntero y, en función del resultado de la comparación con _yes_type, forma el resultado.

Basado en una verificación de una función miembro de una clase, se escribe una verificación de tipo sobre si pertenece al tipo de función. Verificamos si el tipo es de referencia, de lo contrario, buscamos una especialización adecuada de estructuras de sonda de plantilla para un puntero de este tipo, que será true_type para cualquier puntero de función con hasta 24 parámetros.

Y ahora usamos el resultado para implementar is_function . Aquí, por la misma razón que en la parte anterior , no pude heredar esta estructura de integral_constant , por lo que el comportamiento de integral_constant se "simula".

 // is_function template<class _Tp> struct is_function { static const bool value = detail::_is_function_chooser<_Tp, is_reference<_Tp>::value>::value; typedef const bool value_type; typedef integral_constant<bool, is_function::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; 

Y para implementar is_member_function_pointer todavía es más simple:

 // is_member_function_pointer template<class _Tp> struct is_member_function_pointer : public detail::_is_mem_function_ptr_helper<typename remove_cv<_Tp>::type>::type { }; 

Además, en base a estos patrones, podemos determinar si el tipo es un miembro de la clase en principio:

 namespace detail { template<class _Tp> struct _is_member_object_pointer_impl1 : public _not_< _or_<_is_function_ptr_helper<_Tp>, _is_mem_function_ptr_helper<_Tp> > >::type { }; template<class _Tp> struct _is_member_object_pointer_impl2 : public false_type { }; template<class _Tp, class _Cp> struct _is_member_object_pointer_impl2<_Tp _Cp::*> : public true_type { }; template<class _Tp> struct _is_member_object_pointer_helper: public _and_<_is_member_object_pointer_impl1<_Tp>, _is_member_object_pointer_impl2<_Tp> >::type {}; } // is_member_object_pointer template<class _Tp> struct is_member_object_pointer : public detail::_is_member_object_pointer_helper<typename remove_cv<_Tp>::type>::type { }; 

Usó operaciones lógicas 'y', 'o', 'no' en tipos de la primera parte
 namespace detail { struct void_type {}; //typedef void void_type; template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type> struct _or_ : public conditional<_B1::value, _B1, _or_<_B2, _or_<_B3, _B4> > >::type { }; template<> struct _or_<void_type, void_type, void_type, void_type>; template<class _B1> struct _or_<_B1, void_type, void_type, void_type> : public _B1 { }; template<class _B1, class _B2> struct _or_<_B1, _B2, void_type, void_type> : public conditional<_B1::value, _B1, _B2>::type { }; template<class _B1, class _B2, class _B3> struct _or_<_B1, _B2, _B3, void_type> : public conditional<_B1::value, _B1, _or_<_B2, _B3> >::type { }; template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type> struct _and_; template<> struct _and_<void_type, void_type, void_type, void_type>; template<class _B1> struct _and_<_B1, void_type, void_type, void_type> : public _B1 { }; template<class _B1, class _B2> struct _and_<_B1, _B2, void_type, void_type> : public conditional<_B1::value, _B2, _B1>::type { }; template<class _B1, class _B2, class _B3> struct _and_<_B1, _B2, _B3, void_type> : public conditional<_B1::value, _and_<_B2, _B3>, _B1>::type { }; template<class _Pp> struct _not_ { static const bool value = !bool(_Pp::value); typedef const bool value_type; typedef integral_constant<bool, _not_::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; } 


Aquí usamos operaciones lógicas en tipos que, con la ayuda de la plantilla condicional , eventualmente seleccionan el tipo de plantilla apropiado. La programación de plantillas en todo su esplendor, como resultado, en la etapa de compilación, ya tenemos información sobre si el tipo es miembro de la clase. ¡Muy "furioso", pero qué espectacular y efectivo!

Una programación de plantillas un poco más pura en los mismos elementos lógicos y tenemos is_fundamental , is_compound , etc. Signos (esto me deleita, ¿pero tú?):

 // is_arithmetic template<class _Tp> struct is_arithmetic : public detail::_or_<is_integral<_Tp>, is_floating_point<_Tp> >::type { }; // is_fundamental template<class _Tp> struct is_fundamental : public detail::_or_<is_arithmetic<_Tp>, is_void<_Tp>, is_null_pointer<_Tp> >::type {}; // is_object template<class _Tp> struct is_object : public detail::_not_< detail::_or_< is_function<_Tp>, is_reference<_Tp>, is_void<_Tp> > >::type {}; // is_scalar template<class _Tp> struct is_scalar : public detail::_or_<is_arithmetic<_Tp>, is_pointer<_Tp>, is_member_pointer<_Tp>, is_null_pointer<_Tp>/*, is_enum<_Tp>*/ >::type {}; // is_compound template<class _Tp> struct is_compound: public detail::_not_<is_fundamental<_Tp> >::type { }; 
Un lector atento notará que la definición de is_enum está comentada. El hecho es que no encontré formas de distinguir enum de otros tipos, pero creo que esto es factible sin el uso de macros dependientes del compilador. Quizás un lector atento y conocedor le dirá su camino o tren de pensamiento al respecto.
Para determinar el hecho de que un tipo es una clase, ahora no se necesita nada más:

 namespace detail { template <class _Tp, bool _IsReference> struct _is_class_helper { typedef integral_constant<bool, false> type; }; template <class _Tp> struct _is_class_helper<_Tp, false> { typedef integral_constant<bool, (is_scalar<_Tp>::value == bool(false)) //&& !is_union<_Tp>::value >::value && (is_array<_Tp>::value == bool(false)) && (is_void<_Tp>::value == bool(false)) && (is_function<_Tp>::value == bool(false))> type; }; } // is_class template<class _Tp> struct is_class : public detail::_is_class_helper<typename remove_cv<_Tp>::type, is_reference<_Tp>::value>::type { }; 

Y todo estaría bien, pero la unión en C ++ no se puede distinguir de una clase en el caso general. Debido a que son muy similares en sus "manifestaciones externas", y no pude verificar las diferencias (por ejemplo, la incapacidad de heredar de la unión ) sin errores de compilación. Quizás alguien le dirá una maniobra difícil para determinar la unión en la compilación, entonces is_class se corresponderá exactamente con el estándar.

En la parte final de este capítulo, hablaré sobre cómo se implementaron std :: decay y std :: common_type , así como sobre lo que queda por agregar a type_traits .

Gracias por su atencion

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


All Articles