Como eu escrevi a biblioteca C ++ 11 padrão ou por que o impulso é tão assustador? Capítulo 4.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. É incluída sua própria implementação do nullptr , que é selecionada no estágio de compilação.

Chegou a hora de type_traits e todo esse "modelo especial de mágica". Nas partes anteriores deste capítulo, examinamos minha implementação dos modelos básicos da biblioteca padrão e, nesta parte, falaremos sobre a combinação da técnica SFINAE com modelos e um pouco sobre geração de código.

Link para o GitHub com o resultado de hoje para impacientes e não leitores:
Compromissos e críticas construtivas são bem-vindos
Mais modelos de C ++ em cat.

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 4. Modelo C ++ "mágico". Continuação


4.3 Ponteiros e tudo-tudo-tudo


Nesta fase, eu só conseguia obter informações sobre se o tipo é uma matriz para std :: is_array e foi possível iniciar modelos para ponteiros. A implementação também foi trivial, mas não sem suposições.

// 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 { }; */ 

Uma especialização de modelo simples para matrizes de um determinado comprimento "captura" todos os tipos de matrizes; no entanto, o problema surge com o tipo incompleto T [] (uma matriz sem especificar o comprimento). O fato é que esse tipo não é definido por alguns compiladores (C ++ Builder) ao especializar um modelo, e ainda não encontrei uma solução universal aqui.

Depois que a biblioteca foi “ensinada” a definir tipos internos, alinhamento na memória de tipos, trabalhar com modificadores de tipos e outras coisas básicas através de modelos em tempo de compilação, chegou a hora de apontadores e referências.

imagem No C ++, dois grupos de ponteiros podem ser distinguidos - ponteiros para membros da classe e ponteiros para outros objetos. Por que essa separação é importante para a implementação adicional da biblioteca padrão? O fato é que os ponteiros para os alunos têm uma diferença significativa de outros indicadores pela presença disso , ou seja, ponteiro para um objeto desta classe. E, por padrão, os ponteiros para um membro da classe têm uma sintaxe separada para definição, são um tipo separado e não podem ser representados por um ponteiro regular. Na prática, isso se traduz no fato de que o tamanho de um ponteiro para um membro da classe geralmente é maior que o tamanho de um ponteiro comum (que == sizeof (void *) ), porque para implementar funções membro virtuais da classe, bem como armazenar esse ponteiro, os compiladores geralmente implementam ponteiros para um membro da classe como uma estrutura (leia sobre funções e estrutura virtuais). A maneira de apresentar indicadores para os alunos é deixada, de acordo com o padrão, a critério do compilador, mas lembraremos dessa diferença de tamanho e apresentação ao considerar mais códigos.

Para definir um ponteiro regular para um objeto, escreveremos um modelo is_pointer simples, bem como um modelo is_lvalue_reference para referências a objetos (separamos o is_rvalue_reference, porque até o 11º padrão não havia operador && , bem como toda a semântica do movimento):

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

Não há mais nada fundamentalmente novo aqui, o mesmo foi feito nas partes anteriores deste capítulo. Vamos continuar definindo ponteiros para objetos - agora vamos olhar para ponteiros para funções.
É importante entender que uma função e uma função membro de uma classe são entidades completamente diferentes de acordo com o padrão:

  • O primeiro ponteiro será normal (um ponteiro para um objeto), o segundo terá um ponteiro para um membro da classe.

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

  • Você pode criar um link para o primeiro (link do objeto), mas não pode criar um segundo link.

 void (&func_ref)(int); //  'func_ref'    'void func(int){}' //-------------------- //   -     
Aqui vou mencionar um pouco sobre geração de código. Como antes do C ++ 11 não havia modelos com um número variável de parâmetros, todos os modelos em que poderia haver um número diferente de parâmetros foram determinados por meio da especialização do modelo principal com um grande número de parâmetros na entrada e sua inicialização pelos parâmetros fictícios padrão. O mesmo se aplica às sobrecargas de funções, como Também não havia macros com um número variável de parâmetros. Como escrever de 60 a 70 linhas do mesmo tipo de especialização de modelos com as mãos, sobrecarregar funções é uma tarefa bastante monótona e inútil, além de estar repleta da possibilidade de cometer um erro.Eu escrevi um gerador simples de código para modelos e sobrecargas de funções para esses fins. Decidi me limitar a definir funções para 24 parâmetros e isso parece bastante complicado no código, mas simples e 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 os tipos familiares para nós no capítulo anterior da técnica SFINAE:

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

Mais algumas macros para conveniência
 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 } 


As macros são definidas para que seja relativamente conveniente redefinir TYPES e ARGS define como uma lista de tipos e parâmetros, substituindo a macro _IS_MEM_FUN_PTR para gerar definições para todos os tipos de funções possíveis pelo pré-processador. Também vale a pena prestar atenção ao fato de que, para os compiladores da Microsoft, os contratos de chamada ( __fastcall , __stdcall e __cdecl ) também são importantes , porque com convenções diferentes, as funções serão diferentes, embora tenham o mesmo conjunto de argumentos e valor de retorno. Como resultado, todo esse grandioso design de macro é usado de maneira 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 } 

E agora, para o que foi tudo 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 se um tipo é uma função membro de uma classe, primeiro é verificado se o tipo é uma referência. Em seguida, um ponteiro desse tipo é criado e substituído na função de teste. Usando a técnica SFINAE, o compilador seleciona a sobrecarga necessária das funções do probe para esse ponteiro e, com base no resultado da comparação com _yes_type, forma o resultado.

Com base em uma verificação em uma função membro de uma classe, uma verificação de tipo é escrita por pertencer ao tipo de função. Verificamos se o tipo é referência, caso contrário, procuramos uma especialização adequada de estruturas de análise de modelo para um ponteiro desse tipo, que será true_type para qualquer ponteiro de função com até 24 parâmetros.

E agora usamos o resultado para implementar is_function . Aqui, pelo mesmo motivo da parte anterior , não pude herdar essa estrutura de integral_constant , portanto o comportamento de integral_constant é "simulado".

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

E para implementar is_member_function_pointer , ainda é mais simples:

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

Além disso, com base nesses padrões, podemos determinar se o tipo é um membro da classe em princípio:

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

Utilizou operações lógicas 'e', ​​'ou', 'não' em tipos da primeira 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); } }; } 


Aqui, usamos operações lógicas em tipos que, com a ajuda do modelo condicional , eventualmente selecionam o tipo de modelo apropriado. Como programação de modelos em toda a sua glória, como resultado, no estágio de compilação, já temos informações sobre se o tipo é membro da classe. Muito "furioso", mas como espetacular e eficaz!

Um pouco de programação de modelos mais pura com os mesmos elementos lógicos e temos is_fundamental , is_compound , etc. sinais (isso me encanta, mas você?):

 // 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 { }; 
Um leitor atento perceberá que a definição de is_enum está comentada. O fato é que não encontrei maneiras de distinguir enum de outros tipos, mas acho que isso é viável sem o uso de macros dependentes do compilador. Talvez um leitor atento e experiente lhe diga seu caminho ou linha de pensamento a esse respeito.
Para determinar o fato de que um tipo é uma classe, nada mais é necessário agora

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

E tudo ficaria bem, mas a união em C ++ não pode ser distinguida de uma classe no caso geral. Porque eles são muito semelhantes em suas "manifestações externas" e não pude verificar as diferenças (por exemplo, a incapacidade de herdar da união ) sem erros de compilação. Talvez alguém lhe diga uma manobra complicada para determinar a união na compilação, então is_class corresponderá exatamente ao padrão.

Na parte final deste capítulo, falarei sobre como std :: decay e std :: common_type foram implementados , bem como o que resta a ser adicionado aos type_traits .

Obrigado pela atenção.

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


All Articles