
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çã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 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
consequentemente, o código para determinar se o suporte é trivial:
#if (__cplusplus >= 201103L)

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
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
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
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
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
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> { };
e mais macro mágica
#ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) #else
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_ASSERTGCC:
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.