Полезные новации С++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.

Комментарии

Популярные сообщения из этого блога

Полезные новации С++17. Выражения свертки (Fold expressions)

Перегрузка и специализация шаблонных функций.

Ещё о перегрузке шаблонных функций (Partial ordering)