Colocamos as linhas nos parâmetros do modelo

O C ++ moderno nos trouxe um monte de recursos que anteriormente estavam faltando na linguagem. Para obter um efeito semelhante, inventaram-se muletas deslumbrantes por um longo tempo, consistindo principalmente de calçados muito grandes de padrões e macros (geralmente também autógenos). Mas agora, de tempos em tempos, surge a necessidade de oportunidades que ainda não estão no idioma. E começamos a reinventar designs complexos a partir de modelos e macros novamente, gerá-los e alcançar o comportamento de que precisamos. Esta é apenas uma história.

No último semestre, precisei duas vezes de valores que pudessem ser usados ​​nos parâmetros do modelo. Ao mesmo tempo, eu queria ter nomes legíveis por humanos para esses valores e excluir a necessidade de declarar esses nomes antecipadamente. As tarefas específicas que resolvi foram um problema separado; talvez mais tarde escreva posts separados sobre eles, em algum lugar no hub de "programação anormal". Agora vou falar sobre a abordagem pela qual resolvi esse problema.

Portanto, quando se trata de parâmetros do modelo, podemos usar o tipo ou o valor estático da const. Para a maioria das tarefas, isso é mais do que suficiente. Queremos usar identificadores legíveis por humanos nos parâmetros - declaramos a estrutura, enumeração ou constante e os usamos. Os problemas começam quando não podemos determinar esse identificador antecipadamente e queremos fazê-lo no local.

Seria possível declarar uma estrutura ou classe diretamente no parâmetro template. Isso funcionará mesmo se o modelo não fizer nada com esse parâmetro que exija uma descrição completa da estrutura. Além disso, não podemos controlar o espaço para nome no qual essa estrutura é declarada. E substituições de modelo com aparência completamente idêntica se transformarão em código completamente diferente se essas linhas estiverem em classes ou espaços de nomes vizinhos.

Você precisa usar literais, e de todos os literais em C ++, apenas um literal de caractere e um literal de string podem ser chamados de legíveis. Mas um literal de caractere é limitado a quatro caracteres (ao usar char32_t), e um literal de seqüência de caracteres é uma matriz de caracteres e seu valor não pode ser passado para os parâmetros do modelo.

Acontece algum tipo de círculo vicioso. Você deve declarar algo com antecedência ou usar identificadores inconvenientes. Vamos tentar obter o idioma ao qual ele não está adaptado. E se implementar uma macro que tornará algo adequado para uso em argumentos de modelo de uma string literal?

Vamos fazer a estrutura para a string


Primeiro, vamos criar a base para a string. No C ++ 11, surgiram argumentos de modelo variados.
Declaramos uma estrutura que contém caracteres de string nos argumentos:

template <char... Chars> struct String{}; 

github

Isso funciona. Podemos até usar imediatamente essas linhas assim:

 template <class T> struct Foo {}; Foo<String<'B', 'a', 'r'>> foo; 

Agora arraste esta linha para o tempo de execução


Ótimo. Não seria ruim conseguir o valor dessa string em tempo de execução. Que exista uma estrutura de modelo adicional que extraia argumentos dessa string e faça uma constante a partir deles:

 template <class T> struct Get; template <char... Chars> struct Get<String<Chars...>> { static constexpr char value[] = { Chars... }; }; 

Isso também funciona. Como nossas linhas não contêm '\ 0' no final, você precisa operar com cuidado com essa constante (é melhor, na minha opinião, criar imediatamente uma string_view usando a constante e o tamanho dela nos argumentos do construtor). Pode-se simplesmente adicionar '\ 0' no final da matriz, mas isso não é necessário para minhas tarefas.

Verifique se podemos manipular essas strings


Ok, o que mais você pode fazer com essas cordas? Por exemplo, concatenar:

 template <class A, class B> struct Concatenate; template <char... Chars, char... ExtraChars...> struct Concatenate<String<Chars...>, String<ExtraChars...>> { using type = String<Chars..., ExtraChars...>; }; 

github

Em princípio, você pode executar mais ou menos qualquer operação (eu não tentei, porque não preciso disso, mas só posso imaginar como você pode procurar por uma substring ou mesmo substituí-la).
Agora, temos a pergunta principal, como extrair caracteres de uma string literal em tempo de compilação e colocá-los nos argumentos do modelo.

Desenhe a coruja Escreva uma macro.


Vamos começar com uma maneira de colocar caracteres nos argumentos do modelo, um de cada vez:

 template <class T, char c> struct PushBackCharacter; template <char... Chars, char c> struct PushBackCharacter<String<Chars...>, c> { using type = String<Chars..., c>; }; template <char... Chars> struct PushBackCharacter<String<Chars...>, '\0'> { using type = String<Chars...>; }; 

github

Eu uso uma especialização separada para o caractere '\ 0', para não adicioná-lo à string usada. Além disso, isso simplifica as demais partes da macro.

A boa notícia é que um literal de cadeia de caracteres pode ser um parâmetro para a função constexpr. Escrevemos uma função que retorna um caractere por índice em uma string ou '\ 0' se a string for menor que o índice (aqui é útil a especialização PushBackCharacter para o caractere '\ 0').

 template <size_t N> constexpr char CharAt(const char (&s)[N], size_t i) { return i < N ? s[i] : '\0'; } 

Basicamente, já podemos escrever algo como isto:

 PushBackCharacter< PushBackCharacter< PushBackCharacter< PushBackCharacter< String<>, CharAt("foo", 0) >::type, CharAt("foo", 1) >::type, CharAt("foo", 2) >::type, CharAt("foo", 3) >::type 

Colocamos esse calçado, mas mais genuíno (podemos escrever scripts para gerar código) dentro de nossa macro, e é isso!

Há uma nuance. Se o número de caracteres na linha for maior que os níveis de aninhamento na macro, a linha será simplesmente cortada e nem perceberemos. A bagunça.

Vamos criar mais uma estrutura, que não converte a string que chega a ela de nenhuma maneira, mas faz static_assert para que seu comprimento não exceda uma constante:

 #define _NUMBER_TO_STR(n) #n #define NUMBER_TO_STR(n) _NUMBER_TO_STR(n) template <class String, size_t size> struct LiteralSizeLimiter { using type = String; static_assert(size <= MAX_META_STRING_LITERAL_SIZE, "at most " NUMBER_TO_STR(MAX_META_STRING_LITERAL_SIZE) " characters allowed for constexpr string literal"); }; #undef NUMBER_TO_STR #undef _NUMBER_TO_STR 

Bem, a macro será mais ou menos assim:

 #define MAX_META_STRING_LITERAL_SIZE 256 #define STR(literal) \ ::LiteralSizeLimiter< \ ::PushBackCharacter< \ ... \ ::PushBackCharacter< \ ::String<> \ , ::CharAt(literal, 0)>::type \ ... \ , ::CharAt(literal, 255)>::type \ , sizeof(literal) - 1>::type 

github

Acabou


 template <class S> std::string_view GetContent() { return std::string_view(Get<S>::value, sizeof(Get<S>::value)); } std::cout << GetContent<STR("Hello Habr!")>() << std::endl; 

A implementação que obtive pode ser encontrada no github .

Seria muito interessante para mim ouvir sobre as possíveis aplicações desse mecanismo, diferentes daquelas que eu criei.

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


All Articles