
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. Esto completa la descripción de
core.h , pero no estaría completa sin
nullptr .
Enlace a GitHub con el resultado de hoy para impacientes y no lectores:
Los compromisos y las críticas constructivas son bienvenidos
Entonces, continuemos.
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 3. Encontrar la implementación nullptr perfecta
Después de toda la épica con macros compiladores no estándar y los descubrimientos "maravillosos" que presentaron, finalmente pude agregar
nullptr y eso me calentó el alma. Finalmente, puede deshacerse de todas estas comparaciones con 0 o incluso
NULL .

La mayoría de los programadores implementan
nullptr como
#define nullptr 0
y esto podría haber terminado este capítulo. Si desea que sea
nullptr , simplemente reemplace 0 con dicha definición, porque en esencia esto es todo lo que se requiere para la operación correcta.
No olvides escribir realmente un cheque, de lo contrario, de repente se encontrará a alguien más con esta definición:
#ifndef nullptr #define nullptr 0 #else #error "nullptr defined already" #endif
La directiva de preprocesador
#error producirá un error con el texto legible por humanos al compilar, y, sí, esta es una directiva estándar, cuyo uso es raro, pero se puede encontrar.
Pero en tal implementación, perdemos uno de los puntos importantes descritos en el estándar, a saber,
std :: nullptr_t , un tipo separado, una instancia constante de la cual es
nullptr . Y los desarrolladores de cromo una vez también
intentaron resolver este problema (ahora hay un compilador más nuevo y un
nullptr normal) definiéndolo como una clase que puede convertirse en un puntero a cualquier tipo. Dado que, de manera estándar, el tamaño de
nullptr debe ser igual al tamaño del puntero a
anular (y
void * también debe contener cualquier puntero, excepto los punteros a un miembro de la clase), "estandarizamos" esta implementación agregando un puntero nulo no utilizado:
class nullptr_t_as_class_impl { public: nullptr_t_as_class_impl() { } nullptr_t_as_class_impl(int) { }
La conversión de esta clase a cualquier puntero se debe al operador de plantilla del tipo, que se llama si algo se compara con
nullptr . Es decir, la expresión
char * my_pointer; if (my_pointer == nullptr) se convertirá realmente a
if (my_pointer == nullptr.operator char * ()) , que compara el puntero a 0. El segundo tipo de operador es necesario para convertir
nullptr a punteros a miembros de la clase. Y aquí Borland C ++ Builder 6.0 "se distinguió", quien inesperadamente decidió que estos dos operadores son idénticos y pueden comparar fácilmente punteros con un miembro de clase y punteros ordinarios entre sí, por lo que hay una incertidumbre cada vez que
se compara un
nullptr con puntero (esto es un error, y tal vez no sea solo con este compilador). Estamos escribiendo una implementación separada para este caso:
class nullptr_t_as_class_impl1 { public: nullptr_t_as_class_impl1() { } nullptr_t_as_class_impl1(int) { }
Las ventajas de esta vista
nullptr son que ahora hay un tipo separado para
std :: nullptr_t . Desventajas? La constante
nullptr se
pierde durante la compilación y la comparación a través del operador ternario, el compilador no puede resolverlo.
unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr;
Y quiero "y damas y listo". La solución viene a la mente solo una:
enum . Los miembros de la enumeración en C ++ tendrán su propio tipo separado y también se convertirán a
int sin ningún problema (y de hecho son constantes enteras). Esta propiedad de un miembro de enumeración nos ayudará, porque el 0 muy "especial" que se usa en lugar de
nullptr para punteros es el
int más común. No he visto tal implementación de
nullptr en Internet, y quizás también sea algo malo, pero no tenía idea de por qué. Escribamos una implementación:
#ifdef NULL #define STDEX_NULL NULL #else #define STDEX_NULL 0 #endif namespace ptrdiff_detail { using namespace std; } template<bool> struct nullptr_t_as_ulong_type { typedef unsigned long type; }; template<> struct nullptr_t_as_ulong_type<false> { typedef unsigned long type; }; template<bool> struct nullptr_t_as_ushort_type { typedef unsigned short type; }; template<> struct nullptr_t_as_ushort_type<false> { typedef nullptr_t_as_long_type<sizeof(unsigned long) == sizeof(void*)>::type type; }; template<bool> struct nullptr_t_as_uint_type { typedef unsigned int type; }; template<> struct nullptr_t_as_uint_type<false> { typedef nullptr_t_as_short_type<sizeof(unsigned short) == sizeof(void*)>::type type; }; typedef nullptr_t_as_uint_type<sizeof(unsigned int) == sizeof(void*)>::type nullptr_t_as_uint; enum nullptr_t_as_enum { _nullptr_val = ptrdiff_detail::ptrdiff_t(STDEX_NULL), _max_nullptr = nullptr_t_as_uint(1) << (CHAR_BIT * sizeof(void*) - 1) }; typedef nullptr_t_as_enum nullptr_t; #define nullptr nullptr_t(STDEX_NULL)
Como puede ver aquí, un poco más de código que simplemente declarar
enum nullptr_t con el miembro
nullptr = 0 . Primero, puede que no haya definiciones
NULL . Debería definirse en una
lista bastante sólida de encabezados estándar , pero como ha demostrado la práctica, es mejor ir a lo seguro y verificar esta macro. En segundo lugar, la representación
enum en C ++ de acuerdo con el estándar definido por la implementación, es decir el tipo de enumeración puede representarse mediante cualquier tipo de entero (con la condición de que estos tipos no puedan ser más que
int , siempre que los valores de
enumeración se
ajusten a él). Por ejemplo, si declara
enum test {_1, _2}, el compilador puede representarlo fácilmente como
corto, y entonces es muy posible que
sizeof ( test ) ! = Sizeof (void *) . Para que la implementación de
nullptr cumpla con el estándar, debe asegurarse de que el tamaño del tipo que elija el compilador para
nullptr_t_as_enum coincida con el tamaño del puntero, es decir. esencialmente igual
tamaño de (nulo *) . Para hacer esto, usando las plantillas
nullptr_t_as ... , seleccione un tipo de entero que sea igual al tamaño del puntero, y luego establezca el valor máximo del elemento en nuestra enumeración al valor máximo de este tipo de entero.
Quiero prestar atención a la macro CHAR_BIT definida en el encabezado de ascensos estándar. Esta macro se establece en el número de bits en un carácter , es decir El número de bits por byte en la plataforma actual. Una definición estándar útil que los desarrolladores evitan inmerecidamente al pegar ochos en todas partes, aunque en algunos lugares en un byte no hay 8 bits en absoluto .
Y otra característica es la asignación de
NULL como el valor del elemento
enum . Algunos compiladores dan una advertencia (y su preocupación se puede entender) sobre el hecho de que
NULL está asignado al "no indexador".
Eliminamos el
espacio de
nombres estándar para nuestro
ptrdiff_detail local, para no
saturar el resto del espacio de nombres, y luego, para calmar el compilador, convertimos explícitamente
NULL a
std :: ptrdiff_t , otro tipo de alguna manera infrautilizado en C ++, que sirve para representar el resultado de las operaciones aritméticas. (resta) con punteros y suele ser un alias de tipo
std :: size_t (
std :: intptr_t en C ++ 11).
SFINAE
Aquí, por primera vez en mi historia, nos enfrentamos a un fenómeno en C ++, ya que el
error de sustitución no es un error (SFINAE) . En resumen, la esencia de esto es que cuando el compilador "atraviesa" las sobrecargas de funciones apropiadas para una llamada en particular, debe verificarlas todas y no detenerse después de la primera falla o después de la primera sobrecarga adecuada. De aquí viene su mensaje sobre la
ambigüedad , cuando hay dos sobrecargas de la función llamada que son idénticas desde el punto de vista del compilador, así como la capacidad del compilador de seleccionar la sobrecarga de funciones más precisa para una llamada específica con parámetros específicos. Esta característica del compilador le permite hacer la mayor parte de toda la plantilla "mágica" (por cierto hi
std :: enable_if ), y también es la base de boost y mi biblioteca.
Como, como resultado, tenemos varias implementaciones
nullptr, utilizamos SFINAE "select" el mejor en la etapa de compilación. Declaramos los tipos "sí" y "no" para verificar el
tamaño de las funciones de sonda declaradas a continuación.
namespace nullptr_detail { typedef char _yes_type; struct _no_type { char padding[8]; }; struct dummy_class {}; _yes_type _is_convertable_to_void_ptr_tester(void*); _no_type _is_convertable_to_void_ptr_tester(...); typedef void(nullptr_detail::dummy_class::*dummy_class_f)(int); typedef int (nullptr_detail::dummy_class::*dummy_class_f_const)(double&) const; _yes_type _is_convertable_to_member_function_ptr_tester(dummy_class_f); _no_type _is_convertable_to_member_function_ptr_tester(...); _yes_type _is_convertable_to_const_member_function_ptr_tester(dummy_class_f_const); _no_type _is_convertable_to_const_member_function_ptr_tester(...); template<class _Tp> _yes_type _is_convertable_to_ptr_tester(_Tp*); template<class> _no_type _is_convertable_to_ptr_tester(...); }
Aquí usaremos el mismo principio que en el segundo capítulo con countof y su definición a través del tamaño del valor de retorno (matriz de elementos) de la función de plantilla COUNTOF_REQUIRES_ARRAY_ARGUMENT .
template<class T> struct _is_convertable_to_void_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_void_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); };
¿Qué está pasando aquí? Primero, el compilador "
itera " las sobrecargas de la función
_is_convertable_to_void_ptr_tester con un argumento de tipo
T y un valor de
NULL (el valor no juega un papel, solo
NULL debe ser de tipo
T ). Solo hay dos sobrecargas: con el tipo
void * y con la
lista de argumentos variables (...) . Al sustituir un argumento en cada una de estas sobrecargas, el compilador seleccionará el primero si el tipo se convierte en un puntero para
anular , y el segundo si no se puede realizar el lanzamiento. Con la sobrecarga seleccionada por el compilador, usamos
sizeof para determinar el tamaño del valor devuelto por la función, y dado que se garantiza que son diferentes (
sizeof ( _no_type ) == 8 ,
sizeof ( _yes_type ) == 1 ), podemos determinar el tamaño de la sobrecarga que el compilador recogió y, por lo tanto, convierte si nuestro tipo está en
vacío * o no.
Aplicaremos la misma plantilla de programación para determinar si un objeto del tipo de nuestra elección para representar
nullptr_t se
convierte en cualquier puntero (esencialmente
(T) ( STDEX_NULL ) es la definición futura de
nullptr ).
template<class T> struct _is_convertable_to_member_function_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)) && (sizeof(nullptr_detail::_is_convertable_to_const_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class NullPtrType, class T> struct _is_convertable_to_any_ptr_impl_helper { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_ptr_tester<T>((NullPtrType) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class T> struct _is_convertable_to_any_ptr_impl { static const bool value = _is_convertable_to_any_ptr_impl_helper<T, int>::value && _is_convertable_to_any_ptr_impl_helper<T, float>::value && _is_convertable_to_any_ptr_impl_helper<T, bool>::value && _is_convertable_to_any_ptr_impl_helper<T, const bool>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile float>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile const double>::value && _is_convertable_to_any_ptr_impl_helper<T, nullptr_detail::dummy_class>::value; }; template<class T> struct _is_convertable_to_ptr_impl { static const bool value = ( _is_convertable_to_void_ptr_impl<T>::value == bool(true) && _is_convertable_to_any_ptr_impl<T>::value == bool(true) && _is_convertable_to_member_function_ptr_impl<T>::value == bool(true) ); };
Por supuesto, no es posible iterar sobre todos los punteros concebibles e inconcebibles y sus combinaciones con
modificadores volátiles y
constantes , por lo tanto, me limité solo a estos 9 controles (dos en punteros a funciones de clase, uno en puntero a
anular , siete en punteros a diferentes tipos), lo cual es suficiente.
Como se mencionó anteriormente, algunos compiladores (* khe-khe * ... Borland Builder 6.0 ... * khe *) no distinguen entre punteros a un tipo y un miembro de una clase, por lo tanto, escribiremos otra comprobación auxiliar para este caso para que luego podamos seleccionar la implementación deseada de
nullptr_t a través de la clase si es necesario
struct _member_ptr_is_same_as_ptr { struct test {}; typedef void(test::*member_ptr_type)(void); static const bool value = _is_convertable_to_void_ptr_impl<member_ptr_type>::value; }; template<bool> struct _nullptr_t_as_class_chooser { typedef nullptr_detail::nullptr_t_as_class_impl type; }; template<> struct _nullptr_t_as_class_chooser<false> { typedef nullptr_detail::nullptr_t_as_class_impl1 type; };
Y luego solo queda verificar las diferentes implementaciones de
nullptr_t y elegir el compilador apropiado para el compilador.
Elegir la implementación nullptr_t template<bool> struct _nullptr_choose_as_int { typedef nullptr_detail::nullptr_t_as_int type; }; template<bool> struct _nullptr_choose_as_enum { typedef nullptr_detail::nullptr_t_as_enum type; }; template<bool> struct _nullptr_choose_as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type type; }; template<> struct _nullptr_choose_as_int<false> { typedef nullptr_detail::nullptr_t_as_void type; }; template<> struct _nullptr_choose_as_enum<false> { struct as_int { typedef nullptr_detail::nullptr_t_as_int nullptr_t_as_int; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_int>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_int>::value; }; typedef _nullptr_choose_as_int<as_int::_is_convertable_to_ptr == bool(true) && as_int::_equal_void_ptr == bool(true)>::type type; }; template<> struct _nullptr_choose_as_class<false> { struct as_enum { typedef nullptr_detail::nullptr_t_as_enum nullptr_t_as_enum; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_enum>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_enum>::value; static const bool _can_be_ct_constant = true;
Primero, verificamos la posibilidad de representar
nullptr_t como una clase, pero como no encontré un compilador universal de una solución
independiente , no encontré un objeto de tipo que pueda ser una constante del tiempo de compilación (por cierto, estoy abierto a sugerencias sobre este tema, porque es probable que esto sea posible), esta opción siempre está
marcada (
_can_be_ct_constant siempre
es falsa ). A continuación, pasamos a verificar la variante con la vista a través de
enum . Si todavía no fue posible presentar (el compilador no puede presentar un puntero a través de
enum o el tamaño es de alguna manera incorrecto), entonces tratamos de representarlo como un tipo entero (cuyo tamaño será igual al tamaño del puntero a
anular ). Bueno, incluso si esto no funcionó, entonces seleccionamos una implementación del tipo
nullptr_t a través de
void * .
En este punto, se revela la mayor parte del poder de SFINAE en combinación con las plantillas de C ++, debido a lo cual es posible elegir la implementación necesaria sin recurrir a macros dependientes del compilador y, de hecho, a macros (a diferencia de boost, donde todo esto estaría
repleto de
#ifdef #else # cheques endif ).
Solo queda definir un alias de tipo para
nullptr_t en el
espacio de nombres stdex y definir para
nullptr (para cumplir con otro requisito estándar de que no se puede tomar la dirección
nullptr , así como usar
nullptr como una constante de tiempo de compilación).
namespace stdex { typedef detail::_nullptr_chooser::type nullptr_t; } #define nullptr (stdex::nullptr_t)(STDEX_NULL)
El final del tercer capítulo. En el
cuarto capítulo, finalmente llego a type_traits y a los otros errores en los compiladores que encontré durante el desarrollo.
Gracias por su atencion