Como eu escrevi a biblioteca C ++ 11 padrão ou por que o impulso é tão assustador? Capítulo 3

Continuamos a aventura.

Resumo das peças anteriores


Devido a restrições na capacidade de usar compiladores C ++ 11 e, devido à falta de alternância, o boost queria escrever sua própria implementação da biblioteca C ++ 11 padrão na parte superior da biblioteca C ++ 98 / C ++ 03 fornecida com o compilador.

Static_assert , no exceto , countof foram implementados e, também, depois de considerar todos os recursos não padrão e de definição do compilador, surgiram informações sobre a funcionalidade suportada pelo compilador atual. Isso completa a descrição do core.h , mas não seria completo sem o nullptr .

Link para o GitHub com o resultado de hoje para impacientes e não leitores:

Compromissos e críticas construtivas são bem-vindos

Então, vamos continuar.

Sumário


1. Introdução
Capítulo 1. Viam supervadet vadens
Capítulo 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Capítulo 3. Localizando a Implementação NULLPTR Perfeita
Capítulo 4. C ++ Template Magic
.... 4.1 Começamos pequenos
.... 4.2 Sobre quantos erros milagrosos o log compila para nós
.... 4.3 Ponteiros e tudo-tudo-tudo
.... 4.4 O que mais é necessário para a biblioteca de modelos
Capítulo 5
...

Capítulo 3. Localizando a Implementação NULLPTR Perfeita


Depois de todo o épico com macros de compilador não padrão e as descobertas "maravilhosas" que eles apresentaram, finalmente pude adicionar nullptr e isso meio que aqueceu minha alma. Finalmente, você pode se livrar de todas essas comparações com 0 ou mesmo NULL .

imagem A maioria dos programadores implementa o nullptr como
#define nullptr 0 

e isso poderia ter terminado este capítulo. Se você quiser nullptr , substitua 0 por essa definição, porque, em essência, isso é tudo o que é necessário para a operação correta.

Não se esqueça de realmente escrever um cheque; caso contrário, de repente alguém será encontrado com esta definição:

 #ifndef nullptr #define nullptr 0 #else #error "nullptr defined already" #endif 

A diretiva de pré-processador #error produzirá um erro com texto legível por humanos ao compilar e, sim, essa é uma diretiva padrão, cuja utilização é rara, mas pode ser encontrada.

Mas nessa implementação, perdemos um dos pontos importantes descritos no padrão, a saber std :: nullptr_t - um tipo separado, cuja instância constante é nullptr . E os desenvolvedores de cromo também tentaram resolver esse problema (agora há um compilador mais recente e um nullptr normal) definindo-o como uma classe que pode ser convertida em ponteiro para qualquer tipo. Como, por padrão, o tamanho de nullptr deve ser igual ao tamanho do ponteiro para void (e void * também deve conter qualquer ponteiro, exceto ponteiros para um membro da classe), nós "padronizamos" essa implementação adicionando um ponteiro nulo não utilizado:

 class nullptr_t_as_class_impl { public: nullptr_t_as_class_impl() { } nullptr_t_as_class_impl(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } // Make nullptr convertible to any member pointer type. template<typename C, typename T> operator TC::*() { return 0; } bool operator==(nullptr_t_as_class_impl) const { return true; } bool operator!=(nullptr_t_as_class_impl) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl nullptr_t; #define nullptr nullptr_t(0) 

A conversão dessa classe em qualquer ponteiro se deve ao operador de modelo do tipo, chamado se algo for comparado com nullptr . Ou seja, a expressão char * my_pointer; if (my_pointer == nullptr) será realmente convertido em if (my_pointer == nullptr.operator char * ()) , que compara o ponteiro com 0. O operador do segundo tipo é necessário para converter nullptr em ponteiros em membros da classe. E aqui o Borland C ++ Builder 6.0 “se destacou”, que inesperadamente decidiu que esses dois operadores são idênticos e podem facilmente comparar ponteiros com um membro da classe e ponteiros comuns entre si, para que haja uma incerteza toda vez que um nullptr é comparado ponteiro (isso é um bug, e talvez não seja apenas com esse compilador). Estamos escrevendo uma implementação separada para este caso:

 class nullptr_t_as_class_impl1 { public: nullptr_t_as_class_impl1() { } nullptr_t_as_class_impl1(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } bool operator==(nullptr_t_as_class_impl1) const { return true; } bool operator!=(nullptr_t_as_class_impl1) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl1 nullptr_t; #define nullptr nullptr_t(0) 

As vantagens dessa visualização nullptr são que agora existe um tipo separado para std :: nullptr_t . Desvantagens? A constante nullptr é perdida durante a compilação e comparação através do operador ternário, o compilador não pode resolvê-lo.

 unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr; //  ,     ':'    STATIC_ASSERT(nullptr == nullptr && !(nullptr != nullptr), nullptr_should_be_equal_itself); //  , nullptr      

E eu quero "e damas e ir embora". A solução vem à mente apenas uma: enum . Os membros da enumeração em C ++ terão seu próprio tipo separado e também serão convertidos em int sem problemas (e, de fato, são constantes inteiras). Essa propriedade de um membro de enumeração nos ajudará, porque o 0 muito "especial" usado em vez de nullptr para ponteiros é o int mais comum. Eu não vi essa implementação do nullptr na Internet, e talvez também seja algo ruim, mas não tinha nenhuma idéia do porquê. Vamos escrever uma implementação:

 #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 você pode ver aqui um pouco mais de código do que apenas declarar enum nullptr_t com o membro nullptr = 0 . Primeiro, pode não haver definições NULL . Ele deve ser definido em uma lista bastante sólida de cabeçalhos padrão , mas, como a prática demonstrou, é melhor jogar com segurança e verificar essa macro. Em segundo lugar, a representação enum em C ++ de acordo com o padrão definido pela implementação, ou seja, o tipo de enumeração pode ser representado por qualquer tipo de número inteiro (com a condição de que esses tipos não possam ser mais do que int , desde que os valores de enum se ajustem a ele). Por exemplo, se você declarar teste de enumeração {_1, _2}, o compilador pode representá-lo facilmente como curto, e é bem possível que sizeof ( test ) ! = Sizeof (void *) . Para que a implementação nullptr esteja em conformidade com o padrão, é necessário garantir que o tamanho do tipo escolhido pelo compilador para nullptr_t_as_enum corresponda ao tamanho do ponteiro, ou seja, tamanho essencialmente igual (nulo *) . Para fazer isso, usando os modelos nullptr_t_as ... , selecione um tipo inteiro que seja igual ao tamanho do ponteiro e defina o valor máximo do elemento em nossa enumeração para o valor máximo desse tipo inteiro.
Quero prestar atenção à macro CHAR_BIT definida no cabeçalho climits padrão. Essa macro é definida como o número de bits em um caractere , ou seja, o número de bits por byte na plataforma atual. Uma definição padrão útil que os desenvolvedores ignoram imerecidamente colocando oito em todos os lugares, embora em alguns lugares em um byte não haja 8 bits .

E outro recurso é a atribuição de NULL como o valor do elemento enum . Alguns compiladores emitem um aviso (e sua preocupação pode ser entendida) sobre o fato de que NULL é atribuído ao "não indexador". Retiramos o espaço de nomes padrão para o nosso ptrdiff_detail local, para não desordenar o restante do espaço para nome e, para acalmar o compilador, convertemos explicitamente NULL em std :: ptrdiff_t - outro tipo subutilizado em C ++, que serve para representar o resultado de operações aritméticas (subtração) com ponteiros e geralmente é um alias do tipo std :: size_t ( std :: intptr_t no C ++ 11).

SFINAE


Aqui, pela primeira vez na minha história, somos confrontados com um fenômeno em C ++, pois a falha na substituição não é um erro (SFINAE) . Resumindo, a essência disso é que, quando o compilador "passa" pela sobrecarga de função apropriada para uma chamada específica, deve verificar todas elas e não parar após a primeira falha ou após a primeira sobrecarga adequada. Daqui vem sua mensagem sobre ambiguidade , quando há duas sobrecargas da função chamada que são idênticas do ponto de vista do compilador, bem como a capacidade do compilador de selecionar a sobrecarga de função mais precisa para uma chamada específica com parâmetros específicos. Esse recurso do compilador permite que você faça a maior parte de todo o modelo “mágico” (a propósito hi std :: enable_if ), e também é a base do boost e da minha biblioteca.

Como, como resultado, temos várias implementações nullptr, usamos o SFINAE "select" o melhor na fase de compilação. Declaramos os tipos "yes" e "no" para verificar o tamanho das funções do probe declaradas abaixo.

 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(...); } 

Aqui, usaremos o mesmo princípio do segundo capítulo, com countof e sua definição através do tamanho do valor de retorno (matriz de elementos) da função de modelo 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)); }; 

O que está acontecendo aqui? Primeiro, o compilador “ itera ” as sobrecargas da função _is_convertable_to_void_ptr_tester com um argumento do tipo T e um valor NULL (o valor não desempenha um papel, apenas NULL deve ser do tipo T ). Existem apenas duas sobrecargas - com o tipo void * e com a lista de argumentos variáveis ​​(...) . Substituindo um argumento em cada uma dessas sobrecargas, o compilador selecionará o primeiro se o tipo for convertido para um ponteiro a ser anulado e o segundo se o elenco não puder ser executado. Com a sobrecarga selecionada pelo compilador, usamos sizeof para determinar o tamanho do valor retornado pela função e, como é garantido que eles sejam diferentes ( sizeof ( _no_type ) == 8 , sizeof ( _yes_type ) == 1 ), podemos determinar o tamanho da sobrecarga que o compilador captou e, portanto, converte se nosso tipo está inválido * ou não.

Aplicaremos ainda o mesmo modelo de programação para determinar se um objeto do tipo de nossa escolha para representar nullptr_t é convertido em qualquer ponteiro (essencialmente (T) ( STDEX_NULL ) é a definição futura para 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) ); }; 

É claro que não é possível iterar sobre todos os ponteiros concebíveis e inconcebíveis e suas combinações com modificadores voláteis e const ; portanto, eu me limitei apenas a essas 9 verificações (duas em ponteiros para funções de classe, uma em ponteiro para anular , sete em ponteiros para tipos diferentes), o que é suficiente.

Como mencionado acima, alguns compiladores (* khe-khe * ... Borland Builder 6.0 ... * khe *) não fazem distinção entre ponteiros para um tipo e um membro de uma classe, portanto, escreveremos outra verificação auxiliar para esse caso, para que possamos selecionar a implementação desejada de nullptr_t através da classe se necessário.

 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; }; 

E resta apenas verificar as diferentes implementações de nullptr_t e escolher o compilador apropriado para o compilador.

Escolhendo a implementação 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;//_nullptr_can_be_ct_constant_impl<nullptr_t_as_enum>::value; }; typedef _nullptr_choose_as_enum<as_enum::_is_convertable_to_ptr == bool(true) && as_enum::_equal_void_ptr == bool(true) && as_enum::_can_be_ct_constant == bool(true)>::type type; }; struct _nullptr_chooser { struct as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type nullptr_t_as_class; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_class>::value; static const bool _can_be_ct_constant = _nullptr_can_be_ct_constant_impl<nullptr_t_as_class>::value; }; typedef _nullptr_choose_as_class<as_class::_equal_void_ptr == bool(true) && as_class::_can_be_ct_constant == bool(true)>::type type; }; 


Primeiro, verificamos a possibilidade de representar nullptr_t como uma classe, mas como não encontrei um compilador universal de uma solução independente , não encontrei um objeto de tipo que possa ser uma constante de tempo de compilação (a propósito, estou aberto a sugestões sobre esse assunto, porque é provável que isso seja possível). esta opção está sempre marcada ( _can_be_ct_constant é sempre falso ). Em seguida, passamos a verificar a variante com a exibição através de enum . Se ainda não foi possível apresentar (o compilador não pode apresentar um ponteiro através de enum ou o tamanho está de alguma forma errado), tentamos representá-lo como um tipo inteiro (cujo tamanho será igual ao tamanho do ponteiro a ser anulado ). Bem, mesmo que isso não funcionasse, então selecionamos uma implementação do tipo nullptr_t via void * .

Neste ponto, é revelada a maior parte do poder do SFINAE em combinação com modelos C ++, devido ao qual é possível escolher a implementação necessária sem recorrer a macros dependentes do compilador e, de fato, a macros (ao contrário do boost, onde tudo isso seria repleto de verificações #ifdef #else # endif ).

Resta apenas definir um alias de tipo para nullptr_t no espaço de nomes stdex e definir para nullptr (a fim de cumprir outro requisito padrão de que o endereço nullptr não pode ser utilizado, bem como usar nullptr como uma constante de tempo de compilação).

 namespace stdex { typedef detail::_nullptr_chooser::type nullptr_t; } #define nullptr (stdex::nullptr_t)(STDEX_NULL) 

O final do terceiro capítulo. No quarto capítulo, finalmente chego ao type_traits e quais outros bugs nos compiladores me deparei durante o desenvolvimento.

Obrigado pela atenção.

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


All Articles