
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çãoCapítulo 1. Viam supervadet vadensCapítulo 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endifCapítulo 3. Localizando a Implementação NULLPTR PerfeitaCapí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 modelosCapí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 .

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