Dedução de Argumento do Modelo de Classe


O padrão C ++ 17 adicionou um novo recurso ao idioma: Dedução de Argumento de Modelo de Classe (CTAD) . Juntamente com os novos recursos do C ++, tradicionalmente adicionavam novas maneiras de fotografar seus próprios membros. Neste artigo, entenderemos o que é o CTAD, para que é usado, como simplifica a vida e quais são as armadilhas.


Vamos começar de longe


Lembre-se de qual é a dedução de argumento de modelo e para que serve. Se você se sentir confiante o suficiente com os modelos C ++, poderá pular esta seção e prosseguir imediatamente para a próxima.


Antes do C ++ 17, a saída dos parâmetros do modelo aplicava-se apenas aos modelos de função. Ao instanciar um modelo de função, você não pode especificar explicitamente os argumentos do modelo que podem ser inferidos a partir dos tipos dos argumentos reais da função. As regras para dedução são bastante complicadas, são dedicadas a toda a seção 17.9.2 no Padrão [temp.deduct] (daqui em diante refiro-me à versão disponível gratuitamente do rascunho do Padrão ; em versões futuras, a numeração da seção pode mudar, por isso recomendo pesquisar pelo código mnemônico especificado em colchetes).


Não analisaremos detalhadamente todos os meandros dessas regras, elas são necessárias apenas pelos desenvolvedores do compilador. Para uso prático, basta lembrar uma regra simples: o compilador pode derivar independentemente os argumentos do modelo de função, se isso puder ser feito sem ambiguidade, com base nas informações disponíveis. Ao derivar tipos de parâmetros de parâmetros de modelo, as transformações padrão são aplicadas como ao chamar uma função regular ( const é descartada de tipos literais, matrizes são reduzidas a ponteiros, referências de função são reduzidas a ponteiros de função, etc.).


template <typename T> void func(T t) { // ... } int some_func(double d) { return static_cast<int>(d); } int main() { const int i = 123; func(i); // func<int> char arr[] = "Some text"; func(arr); // func<char *> func(some_func); // func<int (*)(double)> return 0; } 

Tudo isso simplifica o uso de modelos de função, mas, infelizmente, é completamente inaplicável aos modelos de classe. Ao instanciar modelos de classe, todos os parâmetros de modelo não padrão precisavam ser especificados explicitamente. Em conexão com essa propriedade desagradável, uma família inteira de funções livres com o prefixo make_ apareceu na biblioteca padrão: make_unique , make_shared , make_pair , make_tuple , etc.


 //  auto tup1 = std::tuple<int, char, double>(123, 'a', 40.0); //   auto tup2 = std::make_tuple(123, 'a', 40.0); 

Novo em C ++ 17


No novo Padrão, por analogia com os parâmetros dos modelos de função, os parâmetros dos modelos de classe são derivados dos argumentos dos construtores chamados:


 std::pair pr(false, 45.67); // std::pair<bool, double> std::tuple tup(123, 'a', 40.0); // std::tuple<int, char, double> std::less l; // std::less<void>,     std::less<> l template <typename T> struct A { A(T,T); }; auto y = new A{1, 2}; //  A<int> auto lck = std::lock_guard(mtx); // std::lock_guard<std::mutex> std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); //       template <typename T> struct F { F(T); } std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // F<lambda> 

Imediatamente, vale a pena mencionar as restrições da CTAD que se aplicam no momento do C ++ 17 (talvez essas restrições sejam removidas em versões futuras da Norma):


  • O CTAD não funciona com aliases de modelo:

 template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true}; //   

  • O CTAD não permite saída parcial de argumentos (como isso funciona para a dedução regular de argumentos de modelo ):

 std::pair p{1, 5}; // OK std::pair<double> q{1, 5}; // ,   std::pair<double, int> r{1, 5}; // OK 

Além disso, o compilador não poderá inferir tipos de parâmetros de modelo que não estão explicitamente relacionados aos tipos de argumentos do construtor. O exemplo mais simples é um construtor de contêiner que aceita um par de iteradores:


 template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //     T   It 

O tipo Não está diretamente relacionado ao T , embora os desenvolvedores saibam exatamente como obtê-lo. Para informar ao compilador como gerar tipos diretamente não relacionados, um novo construto de linguagem apareceu no C ++ 17 - o guia de dedução , que discutiremos na próxima seção.


Guias de dedicação


Para o exemplo acima, o guia de dedução ficaria assim:


 template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; 

Aqui, dizemos ao compilador que, para um construtor com dois parâmetros do mesmo tipo, você pode determinar o tipo de T usando a construção std::iterator_traits<It>::value_type . Observe que os guias de dedução estão fora da definição de classe; isso permite que você personalize o comportamento de classes externas, incluindo classes da Biblioteca Padrão C ++.


Uma descrição formal da sintaxe dos guias de dedução é fornecida no Padrão C ++ 17 na seção 17.10 [temp.deduct.guide] :


 [explicit] template-name (parameter-declaration-clause) -> simple-template-id; 

A palavra-chave explícita antes do guia de dedução proíbe usá-la com a inicialização da lista de cópias :


 template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //  MyVector v3 = {dv.begin(), dv.end()}; //   

A propósito, o guia de dedução não precisa ser um modelo:


 template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"}; // S<std::string> 

Algoritmo detalhado do CTAD


As regras formais para derivar argumentos do modelo de classe são descritas em detalhes na cláusula 16.3.1.8 [over.match.class.deduct] do C ++ Standard 17. Vamos tentar descobri-los.


Portanto, temos um modelo tipo C ao qual o CTAD é aplicado. Para escolher qual construtor e com quais parâmetros chamar, para C , muitas funções de modelo são formadas de acordo com as seguintes regras:


  • Para cada construtor Ci , é gerada uma função fictícia de modelo Fi . Os parâmetros do modelo Fi são parâmetros C , seguidos pelos parâmetros do modelo Ci (se houver), incluindo parâmetros com valores padrão. Os tipos de parâmetros da função Fi correspondem aos tipos de parâmetros do construtor Ci . Retorna uma função fictícia Fi tipo C com argumentos correspondentes aos parâmetros do modelo C.

Pseudocódigo:


 template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); }; //    template <typename T, typename U, typename V, typename W = A> C<T, U> Fi(V, W); 

  • Se o tipo C não estiver definido ou nenhum construtor for especificado, as regras acima se aplicarão ao construtor hipotético C () .
  • Uma função fictícia adicional é gerada para o construtor C © ; para isso, eles até criaram um nome especial: candidato à dedução de cópia .
  • Para cada guia de dedução , também é gerada uma função fictícia Fi com parâmetros do modelo e argumentos do guia de dedução e um valor de retorno correspondente ao tipo à direita de -> no guia de dedução (na definição formal, isso é chamado ID-modelo-simples ).

Pseudocódigo:


 template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>; //    template <typename T, typename V> C<typename DT<T>, typename DT<V>> Fi(T,V); 

Além disso, para o conjunto resultante de funções fictícias Fi , as regras usuais para gerar parâmetros de modelo e resolução de sobrecarga são aplicadas com uma exceção: quando uma função fictícia é chamada com uma lista de inicialização que consiste em um único parâmetro do tipo cv U , em que U é a especialização C ou um tipo herdado da especialização C (apenas no caso, vou esclarecer que cv == const volátil ; esse registro significa que os tipos U , const U , U volátil e U volátil const são tratados da mesma maneira), a regra que prioriza o construtor C(std::initializer_list<>) (é ignorada para detalhes da iniciação da lista A documentação pode ser encontrada na cláusula 16.3.1.7 [over.match.list] do C ++ Standard 17). Um exemplo:


 std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,   std::vector<std::vector<int>> 

Finalmente, se foi possível escolher a única função fictícia mais adequada, o construtor ou guia de dedução correspondente é selecionado. Se não houver nenhum adequado, ou existem vários igualmente adequados, o compilador relata um erro.


Armadilhas


O CTAD é usado para inicializar objetos e a inicialização é tradicionalmente uma parte muito confusa da linguagem C ++. Com a adição de uma inicialização uniforme no C ++ 11, as maneiras de tirar a perna apenas aumentaram. Agora você pode chamar o construtor para um objeto com colchetes redondos e encaracolados. Em muitos casos, essas duas opções funcionam da mesma forma, mas nem sempre:


 std::vector v1{8, 15}; // [8, 15] std::vector v2(8, 15); // [15, 15, … 15] (8 ) std::vector v3{8}; // [8] std::vector v4(8); //   

Até agora, tudo parece bastante lógico: v1 e v3 chamam o construtor que recebe std::initializer_list<int> , int é inferido a partir dos parâmetros; A v4 não pode encontrar um construtor que utilize apenas um parâmetro do tipo int . Mas estas ainda são flores, bagas na frente:


 std::vector v5{"hi", "world"}; // [“hi”, “world”] std::vector v6("hi", "world"); // ?? 

v5 , como esperado, será do tipo std::vector<const char*> e inicializado com dois elementos, mas a próxima linha faz algo completamente diferente. Para um vetor, existe apenas um construtor que aceita dois parâmetros do mesmo tipo:


 template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() ); 

graças ao guia de dedução para std::vector "hi" e "world" serão tratados como iteradores, e todos os elementos "entre" serão adicionados a um vetor do tipo std::vector<char> . Se tivermos sorte e essas duas constantes de string estiverem na memória em uma linha, três elementos cairão no vetor: 'h', 'i', '\ x00', mas, muito provavelmente, esse código levará a uma violação da proteção de memória e falha do programa.


Materiais utilizados


Projeto de norma C ++ 17
CTAD
CppCon 2018: Stephan T. Lavavej "Dedução de argumento de modelo de classe para todos"

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


All Articles