Deducción de argumento de plantilla de clase


El estándar C ++ 17 agregó una nueva característica al lenguaje: Deducción de argumentos de plantilla de clase (CTAD) . Junto con las nuevas características en C ++, tradicionalmente se agregaron nuevas formas de fotografiar sus propias extremidades. En este artículo entenderemos qué es CTAD, para qué se utiliza, cómo simplifica la vida y qué escollos contiene.


Empecemos desde lejos


Recuerde de qué se trata la deducción de argumentos de plantilla y para qué sirve. Si se siente lo suficientemente seguro con las plantillas de C ++, puede omitir esta sección y pasar inmediatamente a la siguiente.


Antes de C ++ 17, la salida de los parámetros de plantilla se aplicaba solo a las plantillas de función. Al crear una instancia de una plantilla de función, no puede especificar explícitamente aquellos argumentos de plantilla que se pueden inferir de los tipos de argumentos de la función real. Las reglas para deducir son bastante complicadas, están cubiertas en toda la sección 17.9.2 de la Norma [temp.deduct] (en adelante, me refiero a la versión disponible gratuitamente del borrador de Norma ; en versiones futuras, la numeración de la sección puede cambiar, por lo que recomiendo buscar por el código mnemónico especificado en corchetes).


No analizaremos en detalle todas las complejidades de estas reglas; solo las necesitan los desarrolladores de compiladores. Para un uso práctico, es suficiente recordar una regla simple: el compilador puede derivar independientemente los argumentos de la plantilla de función, si esto se puede hacer sin ambigüedades en función de la información disponible. Cuando se derivan tipos de parámetros de plantilla, las transformaciones estándar se aplican como cuando se llama a una función regular ( const se descarta de los tipos literales, las matrices se reducen a punteros, las referencias de función se reducen a punteros de función, 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; } 

Todo esto simplifica el uso de plantillas de funciones, pero, por desgracia, es completamente inaplicable a las plantillas de clase. Al crear instancias de plantillas de clase, todos los parámetros de plantilla no predeterminados tenían que especificarse explícitamente. Debido a esta propiedad desagradable, una familia completa de funciones gratuitas con el prefijo make_ apareció en la biblioteca estándar: 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); 

Nuevo en C ++ 17


En el nuevo estándar, por analogía con los parámetros de las plantillas de función, los parámetros de las plantillas de clase se derivan de los argumentos de los constructores llamados:


 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> 

Inmediatamente vale la pena mencionar las restricciones de CTAD que se aplican en el momento de C ++ 17 (tal vez estas restricciones se eliminarán en futuras versiones del Estándar):


  • CTAD no funciona con alias de plantilla:

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

  • CTAD no permite la salida parcial de argumentos (cómo funciona esto para la deducción regular de argumentos de plantilla ):

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

Además, el compilador no podrá inferir tipos de parámetros de plantilla que no estén explícitamente relacionados con los tipos de argumentos de constructor. El ejemplo más simple es un constructor de contenedores que acepta un 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 

El tipo No está directamente relacionado con T , aunque los desarrolladores sabemos exactamente cómo obtenerlo. Para decirle al compilador cómo generar tipos directamente no relacionados, apareció una nueva construcción de lenguaje en C ++ 17, la guía de deducción , que discutiremos en la siguiente sección.


Guías de dedicación


Para el ejemplo anterior, la guía de deducción se vería así:


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

Aquí le decimos al compilador que para un constructor con dos parámetros del mismo tipo, puede determinar el tipo de T utilizando la construcción std::iterator_traits<It>::value_type . Tenga en cuenta que las guías de deducción están fuera de la definición de clase, esto le permite personalizar el comportamiento de las clases externas, incluidas las clases de la Biblioteca estándar de C ++.


Una descripción formal de la sintaxis de las guías de deducción se da en C ++ Standard 17 en la sección 17.10 [temp.deduct.guide] :


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

La palabra clave explícita antes de la guía de deducción prohíbe usarla con copy-list-initialization :


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

Por cierto, la guía de deducción no tiene que ser una plantilla:


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

Algoritmo CTAD detallado


Las reglas formales para derivar argumentos de plantilla de clase se describen en detalle en la cláusula 16.3.1.8 [over.match.class.deduct] de C ++ Standard 17. Intentemos resolverlos.


Entonces, tenemos una plantilla tipo C para la cual se aplica CTAD. Para elegir qué constructor y con qué parámetros llamar, para C , se forman muchas funciones de plantilla de acuerdo con las siguientes reglas:


  • Para cada constructor Ci , se genera una función ficticia de plantilla Fi . Los parámetros de la plantilla Fi son parámetros C , seguidos de los parámetros de la plantilla Ci (si los hay), incluidos los parámetros con valores predeterminados. Los tipos de parámetros de la función Fi corresponden a los tipos de parámetros del constructor Ci . Devuelve una función ficticia Fi tipo C con argumentos que coinciden con los parámetros de la plantilla 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); 

  • Si el tipo C no está definido, o no se especifican constructores, las reglas anteriores se aplican al constructor hipotético C () .
  • Se genera una función ficticia adicional para el constructor C © , incluso se le ocurrió un nombre especial: candidato de deducción de copia .
  • Para cada guía de deducción , también se genera una función ficticia Fi con parámetros de plantilla y argumentos de la guía de deducción y un valor de retorno correspondiente al tipo a la derecha de -> en la guía de deducción (en la definición formal se llama simple-template-id ).

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

Además, para el conjunto resultante de funciones ficticias Fi , se aplican las reglas habituales para generar parámetros de plantilla y resolución de sobrecarga con una excepción: cuando se llama a una función ficticia con una lista de inicialización que consta de un único parámetro de tipo cv U , donde U es la especialización C o un tipo heredado de la especialización C (por si acaso, aclararé que cv == const volátil ; dicho registro significa que los tipos U , const U , volátil U y const volátil U se tratan de la misma manera), la regla que da prioridad al constructor C(std::initializer_list<>) (se omite para detalles del inicio de la lista La localización se puede encontrar en la cláusula 16.3.1.7 [over.match.list] de C ++ Standard 17). Un ejemplo:


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

Finalmente, si fue posible elegir la única función ficticia más adecuada, se selecciona el constructor correspondiente o la guía de deducción . Si no hay otros adecuados, o si hay varios igualmente adecuados, el compilador informa un error.


Trampas


CTAD se usa para inicializar objetos, y la inicialización es tradicionalmente una parte muy confusa del lenguaje C ++. Con la adición de una inicialización uniforme en C ++ 11, las formas de dispararle la pierna solo han aumentado. Ahora puede llamar al constructor para un objeto con corchetes redondos y rizados. En muchos casos, ambas opciones funcionan igual, pero no siempre:


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

Hasta ahora, todo parece ser bastante lógico: v1 y v3 llaman al constructor que toma std::initializer_list<int> , int se infiere de los parámetros; v4 no puede encontrar un constructor que tome solo un parámetro de tipo int . Pero estas siguen siendo flores, bayas en frente:


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

v5 , como se esperaba, será del tipo std::vector<const char*> y se inicializará con dos elementos, pero la siguiente línea hace algo completamente diferente. Para un vector, solo hay un constructor que toma dos parámetros del mismo tipo:


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

Gracias a la guía de deducción de std::vector "hi" y "world" se tratarán como iteradores, y todos los elementos que se encuentren "entre" se agregarán a un vector de tipo std::vector<char> . Si tenemos suerte y estas dos constantes de cadena están en la memoria en una fila, entonces tres elementos caerán en el vector: 'h', 'i', '\ x00', pero, muy probablemente, dicho código conducirá a una violación de la protección de la memoria y al bloqueo del programa.


Materiales utilizados


Proyecto de Norma C ++ 17
CTAD
CppCon 2018: Stephan T. Lavavej "Deducción del argumento de plantilla de clase para todos"

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


All Articles