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

Sim - sim, com esse lema, corri para a batalha.

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.

Além dos arquivos de cabeçalho padrão, type_traits , thread , mutex , chrono , nullptr.h foram adicionados que implementam std :: nullptr_t e core.h onde macros relacionadas à funcionalidade dependente do compilador foram adicionadas, além de expandir a biblioteca padrão.

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

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

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 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif


Depois que todo o código foi penteado um pouco e dividido por cabeçalhos “padrão” em um espaço de nomes separado stdex, passei a preencher type_traits , nullptr.he ao longo do mesmo core.h , que continha macros para determinar a versão do padrão usado pelo compilador e suportá-lo Nullptr nativo , char16_t , char32_t e static_assert .

Em teoria, tudo é simples - de acordo com o padrão C ++ (cláusula 14.8), a macro __cplusplus deve ser definida pelo compilador e corresponder à versão do padrão suportado:

C++ pre-C++98: #define __cplusplus 1 C++98: #define __cplusplus 199711L C++98 + TR1: #define __cplusplus 199711L // ??? C++11: #define __cplusplus 201103L C++14: #define __cplusplus 201402L C++17: #define __cplusplus 201703L 

consequentemente, o código para determinar se o suporte é trivial:

 #if (__cplusplus >= 201103L) //  C++ 11   #define _STDEX_NATIVE_CPP11_SUPPORT //   11  (nullptr, static_assert) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT //    char16_t, char32_t #endif 

imagem De fato, nem tudo é tão simples e agora começam muletas interessantes com um ancinho.

Primeiro, nem todos, ou melhor, nenhum dos compiladores não implementam o próximo padrão completa e imediatamente. Por exemplo, no Visual Studio 2013, o constexpr esteve ausente por muito tempo, enquanto foi reivindicado que ele suporta C ++ 11 - com a ressalva de que a implementação não está completa. Ou seja, auto - por favor, static_assert - é tão fácil (mesmo do MS VS anterior), mas constexpr não é. Em segundo lugar, nem todos os compiladores (e isso é ainda mais surpreendente) expõem corretamente essa definição e a atualizam em tempo hábil. Inesperadamente, no mesmo compilador, o Visual Studio não alterou a versão do __cplusplus define desde as primeiras versões do compilador, embora o suporte completo ao C ++ 11 tenha sido declarado há muito tempo (o que também não é verdadeiro, para o qual existem raios separados de descontentamento - assim que a conversa chega à funcionalidade específica do “novo "11 desenvolvedores padrão dizem imediatamente que não há pré-processador C99, não há outros" recursos "). E a situação é agravada pelo fato de que, pelos compiladores padrão, podem definir isso como diferente dos valores acima, se eles não cumprirem totalmente os padrões declarados. Seria lógico supor, por exemplo, um desenvolvimento de define para uma determinada macro (com a introdução de novas funcionalidades, aumente o número oculto por trás dessa definição):

 standart C++98: #define __cplusplus 199711L // C++98 standart C++98 + TR1: #define __cplusplus 200311L // C++03 nonstandart C++11: #define __cplusplus 200411L // C++03 + auto and dectype nonstandart C++11: #define __cplusplus 200511L // C++03 + auto, dectype and constexpr(partly) ... standart C++11: #define __cplusplus 201103L // C++11 

Mas, ao mesmo tempo, nenhum dos principais compiladores populares está "desgastado" com esse recurso.

Por causa de tudo isso (não tenho medo dessa palavra), agora para cada compilador não padrão, você deve escrever suas próprias verificações específicas para descobrir qual padrão C ++ e em que extensão ele suporta. A boa notícia é que precisamos aprender apenas algumas funções do compilador para funcionar corretamente. Primeiro, agora adicionamos a verificação de versão do Visual Studio por meio da macro _MSC_VER , exclusiva deste compilador. Como no meu arsenal de compiladores suportados, há também o C ++ Borland Builder 6.0, cujos desenvolvedores, por sua vez, estavam muito interessados ​​em manter a compatibilidade com o Visual Studio (incluindo seus "recursos" e bugs), então de repente existe essa macro também. Para compiladores compatíveis com clang, existe uma macro não padrão __has_feature ( feature_name ) , que permite descobrir se o compilador suporta essa ou aquela funcionalidade. Como resultado, o código é inflado para:

 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif 

Deseja alcançar mais compiladores? Adicionamos verificações para o Codegear C ++ Builder, que é o herdeiro da Borland (em suas piores manifestações, mas mais sobre isso mais tarde):

 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT) #if ((__cplusplus > 199711L) || defined(__CODEGEARC__)) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif 

Também é importante observar que, como o Visual Studio já implementou o suporte a nullptr da versão do compilador _MSC_VER 1600 , bem como os tipos internos char16_t e char32_t , precisamos lidar com isso corretamente. Mais algumas verificações adicionadas:

 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT) #if ((__cplusplus > 199711L) || defined(__CODEGEARC__)) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif #if ((!defined(_MSC_VER) || _MSC_VER < 1600) && !defined(_STDEX_NATIVE_CPP11_SUPPORT)) #define _STDEX_IMPLEMENTS_NULLPTR_SUPPORT #else #define _STDEX_NATIVE_NULLPTR_SUPPORT #endif #if (_MSC_VER >= 1600) #ifndef _STDEX_NATIVE_CPP11_TYPES_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif 

Ao mesmo tempo, verificaremos o suporte ao C ++ 98, pois para compiladores sem ele não haverá alguns arquivos de cabeçalho da biblioteca padrão e não podemos verificar a ausência deles usando o compilador.

Opção completa
 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT) #if ((__cplusplus > 199711L) || defined(__CODEGEARC__)) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif #if ((!defined(_MSC_VER) || _MSC_VER < 1600) && !defined(_STDEX_NATIVE_CPP11_SUPPORT)) #define _STDEX_IMPLEMENTS_NULLPTR_SUPPORT #else #define _STDEX_NATIVE_NULLPTR_SUPPORT #endif #if (_MSC_VER >= 1600) #ifndef _STDEX_NATIVE_CPP11_TYPES_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif #if _MSC_VER // Visual C++ fallback #define _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT #define _STDEX_CDECL __cdecl #if (__cplusplus >= 199711L) #define _STDEX_NATIVE_CPP_98_SUPPORT #endif #endif // C++ 98 check: #if ((__cplusplus >= 199711L) && ((defined(__INTEL_COMPILER) || defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4)))))) #ifndef _STDEX_NATIVE_CPP_98_SUPPORT #define _STDEX_NATIVE_CPP_98_SUPPORT #endif #endif 


E agora configurações volumosas do impulso estão começando a aparecer na minha memória, na qual muitos desenvolvedores esforçados escreveram todas essas macros dependentes do compilador e fizeram um mapa do que é suportado e o que não é de um compilador específico de uma versão específica, da qual eu pessoalmente me sinto desconfortável, Eu quero nunca mais olhar para ele ou tocá-lo. Mas a boa notícia é que você pode parar por aí. Pelo menos, isso é suficiente para eu oferecer suporte aos compiladores mais populares, mas se você encontrar uma imprecisão ou quiser adicionar outro compilador, ficarei feliz em aceitar a solicitação de recebimento.

Uma grande conquista em comparação ao impulso, acredito que foi possível manter a difusão de macros dependentes do compilador pelo código, o que torna o código mais limpo e fácil de entender, além de não empilhar dezenas de arquivos de configuração para cada sistema operacional e para cada compilador. Falaremos sobre as desvantagens dessa abordagem um pouco mais tarde.

Nesse estágio, já podemos começar a conectar a funcionalidade ausente dos 11 padrões, e a primeira coisa que apresentamos é static_assert .

static_assert


Definimos a estrutura StaticAssertion , que assumirá um valor booleano como parâmetro do modelo - haverá nossa condição, se não for atendida (a expressão é falsa ), ocorrerá um erro na compilação de um modelo não especializado. E outra estrutura fictícia para receber sizeof ( StaticAssertion ) .

 namespace stdex { namespace detail { template <bool> struct StaticAssertion; template <> struct StaticAssertion<true> { }; // StaticAssertion<true> template<int i> struct StaticAssertionTest { }; // StaticAssertionTest<int> } } 

e mais macro mágica

 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) #else // no C++11 support #define CONCATENATE(arg1, arg2) CONCATENATE1(arg1, arg2) #define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2) #define CONCATENATE2(arg1, arg2) arg1##arg2 #define STATIC_ASSERT(expression, message)\ struct CONCATENATE(__static_assertion_at_line_, __LINE__)\ {\ stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\ };\ typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__) #ifndef _STDEX_NATIVE_NULLPTR_SUPPORT #define static_assert(expression, message) STATIC_ASSERT(expression, ERROR_MESSAGE_STRING) #endif #endif 

uso:

 STATIC_ASSERT(sizeof(void*) == 4, non_x32_platform_is_unsupported); 

Uma diferença importante entre minha implementação e a padrão é que não há sobrecarga dessa palavra-chave sem informar o usuário. Isso se deve ao fato de que em C ++ é impossível definir várias definições com um número diferente de argumentos, exceto um nome, e uma implementação sem uma mensagem é muito menos útil que a opção selecionada. Esse recurso leva ao fato de que, em essência, STATIC_ASSERT na minha implementação já é a versão adicionada no C ++ 11.
Vamos dar uma olhada no que aconteceu. Como resultado da verificação das versões das __cplusplus e das macros de compilador não padrão, temos informações suficientes sobre o suporte ao C ++ 11 (e, portanto, static_assert ), expressas pela definição _STDEX_NATIVE_CPP11_SUPPORT. Portanto, se essa macro estiver definida, podemos simplesmente usar o static_assert padrão:

 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) 

Observe que o segundo parâmetro da macro STATIC_ASSERT não é literalmente uma string e , portanto, usando o operador do pré-processador # , converteremos o parâmetro de mensagem em uma string para transmissão ao static_assert padrão.
Se não tivermos suporte do compilador, prosseguiremos para nossa implementação. Para começar, declararemos macros auxiliares para "colar" as strings (o operador de pré-processador ## é o único responsável por isso).

 #define CONCATENATE(arg1, arg2) CONCATENATE1(arg1, arg2) #define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2) #define CONCATENATE2(arg1, arg2) arg1##arg2 

Especificamente, não usei simplesmente #define CONCATENATE ( arg1 , arg2 ) arg1 ## arg2 para poder passar o resultado da mesma macro CONCATENATE como argumento para arg1 e arg2 .
Em seguida, declaramos uma estrutura com o nome bonito __static_assertion_at_line_ {número da linha} (a macro __LINE__ também é definida pelo padrão e deve ser expandida para o número da linha na qual foi chamada) e, dentro dessa estrutura, adicionamos um campo do nosso tipo StaticAssertion com o nome STATIC_ASSERTION_FAILED_AT_LINE_ {número da linha} _WITH. mensagens de erro da macro de chamada}.

 #define STATIC_ASSERT(expression, message)\ struct CONCATENATE(__static_assertion_at_line_, __LINE__)\ {\ stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\ };\ typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__) 

Com o parâmetro de modelo em StaticAssertion, passamos uma expressão que é verificada em STATIC_ASSERT , levando-a a bool . Finalmente, para evitar a criação de variáveis ​​locais e a verificação de sobrecarga zero da condição do usuário, um alias é declarado para o tipo StaticAssertionTest <sizeof ({nome da estrutura declarada acima}) com o nome __static_assertion_test_at_line_ {número da linha}.

Toda a beleza da nomenclatura é necessária apenas para deixar claro a partir de um erro de compilação que este é um resultado de afirmação, e não apenas um erro, mas também para exibir uma mensagem de erro que foi definida para essa afirmação. O truque sizeof é necessário para forçar o compilador a instanciar a classe de modelo StaticAssertion , que está dentro da estrutura recém-declarada e, assim, verificar a condição passada para afirmar.

Resultados STATIC_ASSERT
GCC:
30: 103: erro: o campo 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' possui um tipo incompleto 'stdex :: detail :: StaticAssertion <false>'
25:36: nota: na definição da macro 'CONCATENATE2'
23:36: nota: na expansão da macro 'CONCATENATE1'
30:67: nota: na expansão da macro 'CONCATENATE'
24:36: nota: na expansão da macro 'CONCATENATE2'
23:36: nota: na expansão da macro 'CONCATENATE1'
30:79: nota: na expansão da macro 'CONCATENATE'
24:36: nota: na expansão da macro 'CONCATENATE2'
23:36: nota: na expansão da macro 'CONCATENATE1'
30:91: nota: na expansão da macro 'CONCATENATE'
36: 3: note: na expansão da macro 'STATIC_ASSERT'

Borland C ++ Builder:
[Erro C ++] stdex_test.cpp (36): E2450 Estrutura indefinida 'stdex :: detail :: StaticAssertion <0>'
[Erro C ++] stdex_test.cpp (36): E2449 O tamanho de 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' é desconhecido ou zero
[Erro C ++] stdex_test.cpp (36): E2450 Estrutura indefinida 'stdex :: detail :: StaticAssertion <0>'

Visual Studio:
Erro c2079


O segundo "truque" que eu queria ter, embora ausente do padrão, é contar o número de elementos na matriz. Os Sishers gostam muito de declarar essa macro através de sizeof (arr) / sizeof (arr [0]), mas iremos além.

contagem de


 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #include <cstddef> namespace stdex { namespace detail { template <class T, std::size_t N> constexpr std::size_t _my_countof(T const (&)[N]) noexcept { return N; } } // namespace detail } #define countof(arr) stdex::detail::_my_countof(arr) #else //no C++11 support #ifdef _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT // Visual C++ fallback #include <stdlib.h> #define countof(arr) _countof(arr) #elif defined(_STDEX_NATIVE_CPP_98_SUPPORT)// C++ 98 trick #include <cstddef> template <typename T, std::size_t N> char(&COUNTOF_REQUIRES_ARRAY_ARGUMENT(T(&)[N]))[N]; #define countof(x) sizeof(COUNTOF_REQUIRES_ARRAY_ARGUMENT(x)) #else #define countof(arr) sizeof(arr) / sizeof(arr[0]) #endif 

Para compiladores com suporte ao constexpr , declararemos uma versão constexpr deste modelo (que não é absolutamente necessária, para todos os padrões, a implementação através do modelo COUNTOF_REQUIRES_ARRAY_ARGUMENT é suficiente ); para o resto, apresentaremos a versão através da função de modelo COUNTOF_REQUIRES_ARRAY_ARGUMENT . O Visual Studio aqui novamente se distingue pela presença de sua própria implementação de _countof no arquivo de cabeçalho stdlib.h .

A função COUNTOF_REQUIRES_ARRAY_ARGUMENT parece intimidadora e descobrir o que faz é bastante complicado. Se você observar atentamente, poderá entender que ele usa uma única matriz de elementos do tipo T e tamanho N como argumento - assim, no caso de transferência de outros tipos de elementos (não matrizes), obtemos um erro de compilação, o que sem dúvida agrada. Observando mais de perto, você pode descobrir (com dificuldade) que ele retorna uma matriz de elementos de caracteres do tamanho N. A questão é: por que precisamos de tudo isso? É aqui que o tamanho do operador entra em ação e sua capacidade exclusiva de trabalhar em tempo de compilação. A chamada sizeof ( COUNTOF_REQUIRES_ARRAY_ARGUMENT ) determina o tamanho da matriz de elementos char retornados pela função e, como o sizeof padrão (char) == 1, esse é o número de N elementos na matriz original. Elegante, bonito e totalmente gratuito.

para sempre


Outra macro auxiliar pequena que eu uso sempre que um loop infinito é necessário é para sempre . É definido da seguinte forma:

 #if !defined(forever) #define forever for(;;) #else #define STRINGIZE_HELPER(x) #x #define STRINGIZE(x) STRINGIZE_HELPER(x) #define WARNING(desc) message(__FILE__ "(" STRINGIZE(__LINE__) ") : warning: " desc) #pragma WARNING("stdex library - macro 'forever' was previously defined by user; ignoring stdex macro definition") #undef STRINGIZE_HELPER #undef STRINGIZE #undef WARNING #endif 

Exemplo de sintaxe para definir um loop infinito explícito:

  unsigned int i = 0; forever { ++i; } 

Essa macro é usada apenas para definir explicitamente um loop infinito e é incluída na biblioteca apenas por razões de "adicionar açúcar sintático". No futuro, proponho substituí-lo por opcionalmente através da definição da macro de plug-in FOREVER O que é notável no trecho de código acima da biblioteca é a mesma macro WARNING que gera uma mensagem de aviso em todos os compiladores se a macro forever já tiver sido definida pelo usuário. Ele usa a macro __LINE__ padrão familiar e a macro __FILE__ padrão, que é convertida em uma sequência com o nome do arquivo de origem atual.

stdex_assert


Para implementar a declaração em tempo de execução, a macro stdex_assert é apresentada como:

 #if defined(assert) #ifndef NDEBUG #include <iostream> #define stdex_assert(condition, message) \ do { \ if (! (condition)) { \ std::cerr << "Assertion `" #condition "` failed in " << __FILE__ \ << " line " << __LINE__ << ": " << message << std::endl; \ std::terminate(); \ } \ } while (false) #else #define stdex_assert(condition, message) ((void)0) #endif #endif 

Não direi que estou muito orgulhoso dessa implementação (ela será alterada no futuro), mas uma técnica interessante foi usada aqui que eu gostaria de chamar a atenção. Para ocultar as verificações do escopo do código do aplicativo, é usada a construção do {} while (false) , que será executada, o que é óbvio uma vez e ao mesmo tempo não introduzirá o código de "serviço" no código geral do aplicativo. Essa técnica é bastante útil e é usada em vários outros lugares da biblioteca.

Caso contrário, a implementação é muito semelhante à afirmação padrão - com uma determinada macro NDEBUG , que os compiladores geralmente configuram nas compilações de lançamento, a afirmação não faz nada; caso contrário, interrompe a execução do programa com a saída da mensagem no fluxo de erro padrão se a condição de afirmação não for atendida.

noexcept


Para funções que não geram exceções, a palavra-chave noexcept foi introduzida no novo padrão. Também é bastante simples e fácil de implementar através da macro:

 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #define stdex_noexcept noexcept #else #define stdex_noexcept throw() #endif 

no entanto, é necessário entender que, no padrão, o noexcept pode levar o valor bool e também ser usado para determinar, em tempo de compilação, que a expressão transmitida a ele não gera uma exceção. Essa funcionalidade não pode ser implementada sem o suporte do compilador e, portanto, existe apenas um stdex_noexcept "despojado" na biblioteca.

O final do segundo capítulo. O terceiro capítulo abordará os meandros da implementação do nullptr, por que é diferente para diferentes compiladores, bem como o desenvolvimento de type_traits e quais outros erros nos compiladores me deparei durante seu desenvolvimento.

Obrigado pela atenção.

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


All Articles