Полезные новации С++17. Вывод аргументов шаблона для конструкторов шаблонных классов.
Вероятно, вы часто использовали в своих программах вызовы шаблонных функций, подобных std::make_pair или std::make_move_iterator, при этом удивляясь необходимости использования вспомогательной функции для создания нужного объекта. Проблема заключается в отсутствии вывода шаблонных аргументов для конструкторов шаблонных классов. Рассмотрим простой класс pair:
template <typename T, typename U>
struct pair {
T t;
U u;
pair (const T& t, const U& u) : t{t}, u{u} {}
};
Поскольку отсутствует вывод аргументов для имеющегося конструктора, нам необходимо явно специфицировать шаблонные аргументы при конструировании pair, например вот так:
pair<int, float> a{1, 0.5f};
Было бы удобнее иметь возможность написать так:
pair a{1, 0.5f};
но это невозможно. Общим решением является предоставление вспомогательной функции, которая выполняет выведение типов:
template <typename T, typename U>
pair<T,U> make_pair (const T& t, const U& u) {
return {t,u};
}
Теперь вы можете написать
auto p = make_pair(0, 0.5f);
Однако - это не слишком привлекательный вариант. Предполагается, что вы имеете различные пути для конструирования специализаций шаблонных классов и нормальных классов, вы будете часто заглядывать в документацию, чтобы выяснить пути конструирования объектов, не-копируемые и не-перемещаемые классы становятся головной болью, и т.д. Было бы гораздо проще, если бы мы могли просто выводить шаблонные аргументы в конструкторе.
К счастью, это сделано в С++17. Это позволит нам избежать использования make_ функций и непосредственно конструировать объекты:
pair a{0, 42.42};
std::tuple t{"hi", 15, std::vector<int>{}};
Эта возможность поддерживается через новый концепт, известный как руководство по выведению, который указывает компилятору - как выводить шаблонные аргументы для шаблонного класса из аргументов конструктора. Они могут быть неявно синтезированы, либо явно специфицированы.
Руководства по выведению могут быть сгенерированы из существующих конструкторов без каких-либо указаний программиста, до тех пор пока они достаточно просты.
Хорошее практическое правило: если все параметры ваших конструкторов включают все шаблонные параметры классов в контекстах выведения - вам не нужно ничего делать.
Вернемся к нашему примеру pair. Конструкторы класса pair, включая генерируемые компилятором:
template <typename T, typename U>
struct pair {
pair (const T& t, const U& u);
pair (const pair<T,U>&);
pair (pair<T,U>&&);
};
Во всех трех случаях, оба шаблонных аргумента для pair (T и U) выводимы из параметров конструктора, поэтому нам не нужно ничего делать. Последующий код заработает сразу:
pair a {0, 42.42};
pair b = a;
pair c = std::move(b);
Если ваш класс не выполняет требований руководств по неявному выведению и вы хотите выводить шаблонные аргументы класса автоматически - вы можете немного помочь компилятору.
Например, мы имеем простой класс MyVector и хотим сконструировать MyVector из первых трех элементов другого объекта. Мы могли бы сделать так:
template <typename T>
class MyVector
{
std::vector<T> internalVect;
public:
MyVector(std::initializer_list<T> a) :
internalVect(a) {}
template <typename Iter>
MyVector(Iter start, Iter end)
:internalVect(start, end)
{}
T* begin()
{
auto It = internalVect.begin();
T* p = &(*It);
return p;
}
T* end()
{
...............
}
};
................
MyVector a({ 1, 2, 3, 4, 5, 6, 7 }); // Uses implicit deduction guide
auto it = a.begin();
MyVector b(it, it + 3); // Implicit guide will not work
Исходя из приведенного практического правила - второй конструктор, работающий с итераторами не может вывести T, поэтому нам необходимо предоставить явное руководство по выведению. Оно выглядит так:
template<typename Iter>
MyVector(Iter b, Iter e)->MyVector < typename std::iterator_traits<Iter>::value_type > ;
Приведенное руководство по выведению говорит (компилятору), что конструктор MyVector, принимающий два Iter, выводит шаблонный аргумент типа
typename std::iterator_traits<Iter>::value_type. Для int*, это int. Когда компилятор видит
MyVector b(it, it + 3);
он инстанцирует руководство по выведению с int*, как аргументом шаблона и выводит
MyVector<int> как тип b;
Полный пример:
#include <iostream>
#include <vector>
#include <initializer_list>
template <typename T>
class MyVector
{
std::vector<T> internalVect;
public:
MyVector(std::initializer_list<T> a) :
internalVect(a) {}
T& get(int i)
{
return internalVect.at(i);
}
template <typename Iter>
MyVector(Iter start, Iter end)
:internalVect(start, end)
{}
T* begin()
{
auto It = internalVect.begin();
T* p = &(*It);
return p;
}
T* end()
{
auto It = internalVect.end();
T* p = &(*It);
return p;
}
};
template<typename Iter>
MyVector(Iter b, Iter e)->MyVector < typename std::iterator_traits<Iter>::value_type > ;
int main()
{
MyVector a({ 1, 2, 3, 4, 5, 6, 7 }); // Uses implicit deduction guide
std::cout << a.get(0) << '\n';
auto it = a.begin();
MyVector b(it, it + 3); // Implicit guide will not work
std::cout << b.get(1) << '\n';
return 0;
}
Подробности:
https://tech.io/playgrounds/2205/7-features-of-c17-that-will-simplify-your-code/template-argument-deduction-for-class-templates
https://blog.tartanllama.xyz/deduction-for-class-templates/
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0091r3.html
https://arne-mertz.de/2017/06/class-template-argument-deduction/#User-defined_deduction_guides
P.S.
На текущий момент вывод аргументов шаблона для конструкторов шаблонных классов поддерживается GCC 7.
template <typename T, typename U>
struct pair {
T t;
U u;
pair (const T& t, const U& u) : t{t}, u{u} {}
};
pair<int, float> a{1, 0.5f};
Было бы удобнее иметь возможность написать так:
pair<T,U> make_pair (const T& t, const U& u) {
return {t,u};
}
std::tuple t{"hi", 15, std::vector<int>{}};
Неявно синтезированные руководства по выведению.
Руководства по выведению могут быть сгенерированы из существующих конструкторов без каких-либо указаний программиста, до тех пор пока они достаточно просты.
Хорошее практическое правило: если все параметры ваших конструкторов включают все шаблонные параметры классов в контекстах выведения - вам не нужно ничего делать.
Вернемся к нашему примеру pair. Конструкторы класса pair, включая генерируемые компилятором:
struct pair {
pair (const T& t, const U& u);
pair (const pair<T,U>&);
pair (pair<T,U>&&);
};
Во всех трех случаях, оба шаблонных аргумента для pair (T и U) выводимы из параметров конструктора, поэтому нам не нужно ничего делать. Последующий код заработает сразу:
pair a {0, 42.42};
pair b = a;
pair c = std::move(b);
Если ваш класс не выполняет требований руководств по неявному выведению и вы хотите выводить шаблонные аргументы класса автоматически - вы можете немного помочь компилятору.
Явно специфицированные руководства по выведению.
Руководства по выведению могут быть явно специфицированы для указания компилятору - как выработать шаблонные аргументы из аргументов конструктора, в случае, если они не могут просто соответствовать.Например, мы имеем простой класс MyVector и хотим сконструировать MyVector из первых трех элементов другого объекта. Мы могли бы сделать так:
template <typename T>
class MyVector
{
std::vector<T> internalVect;
public:
MyVector(std::initializer_list<T> a) :
internalVect(a) {}
template <typename Iter>
MyVector(Iter start, Iter end)
:internalVect(start, end)
{}
T* begin()
{
auto It = internalVect.begin();
T* p = &(*It);
return p;
}
T* end()
{
...............
}
};
................
MyVector a({ 1, 2, 3, 4, 5, 6, 7 }); // Uses implicit deduction guide
auto it = a.begin();
MyVector b(it, it + 3); // Implicit guide will not work
Исходя из приведенного практического правила - второй конструктор, работающий с итераторами не может вывести T, поэтому нам необходимо предоставить явное руководство по выведению. Оно выглядит так:
template<typename Iter>
MyVector(Iter b, Iter e)->MyVector < typename std::iterator_traits<Iter>::value_type > ;
#include <vector>
#include <initializer_list>
template <typename T>
class MyVector
{
std::vector<T> internalVect;
public:
MyVector(std::initializer_list<T> a) :
internalVect(a) {}
T& get(int i)
{
return internalVect.at(i);
}
template <typename Iter>
MyVector(Iter start, Iter end)
:internalVect(start, end)
{}
T* begin()
{
auto It = internalVect.begin();
T* p = &(*It);
return p;
}
T* end()
{
auto It = internalVect.end();
T* p = &(*It);
return p;
}
};
template<typename Iter>
MyVector(Iter b, Iter e)->MyVector < typename std::iterator_traits<Iter>::value_type > ;
int main()
{
MyVector a({ 1, 2, 3, 4, 5, 6, 7 }); // Uses implicit deduction guide
std::cout << a.get(0) << '\n';
auto it = a.begin();
MyVector b(it, it + 3); // Implicit guide will not work
std::cout << b.get(1) << '\n';
return 0;
}
https://blog.tartanllama.xyz/deduction-for-class-templates/
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0091r3.html
https://arne-mertz.de/2017/06/class-template-argument-deduction/#User-defined_deduction_guides
Комментарии
Отправить комментарий