Пространства имён
Варианты
Действия

Вывод типов аргументов шаблона класса(начиная с C++17)

Материал из cppreference.com
< cpp‎ | language

Чтобы инстанцировать некоторый шаблон класса, должен быть известен каждый аргумент шаблона, но не каждый должен быть указан. В следующих контекстах компилятор выведет типы отсутствующих аргументов шаблона из типа инициализатора:

  • любое объявление переменной с инициализацией и шаблон переменной
std::pair p(2, 4.5);     // выводится как std::pair<int, double> p(2, 4.5);
std::tuple t(4, 3, 2.5); // эквивалентно auto t = std::make_tuple(4, 3, 2.5);
std::less l;             // эквивалентно std::less<void> l;
template<class T> struct A { A(T,T); };
auto y = new A{1,2}; // allocated type is A<int>
auto lck = std::lock_guard(mtx); // выводится как std::lock_guard<std::mutex>
std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); // выводится как std::back_inserter(vi2)
std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // выводится как Foo<T>, где T 
                                                            // уникальный тип лямбды
template<class T>
struct X {
    X(T) {}
    auto operator<=>(const X&) const = default;
};
 
template<X x> struct Y { };
 
Y<0> y; // OK, Y<X<int>(0)>
(начиная с C++20)

Содержание

[править] Неявно сгенерированные правила вывода типов

Если приведение в функциональном стиле или объявление переменной использует имя первичного шаблона класса C без списка аргументов как спецификатор типа, вывод типа производится следующим образом:

  • Если C определен, для каждого конструктора (или шаблонного конструктора) Ci, объявленного в именованном первичном шаблоне (если он определен), создаётся воображаемый (fictional) шаблон функции Fi, такой что:
  • параметры шаблона функции Fi являются параметрами шаблона класса C после которого следуют параметры шаблона Ci, если Ci является шаблонным конструктором. В определение также включаются аргументы шаблона по-умолчанию.
  • параметры функции Fi являются параметрами конструктора
  • возвращаемым Fi типом является C, после которого внутри <> следуют параметры шаблона класса
  • Если C не определён или в нём не объявлено ни одного конструктора, на основе гипотетического конструктора C() добавляется дополнительный воображаемый шаблон функции, определяемый как показано выше
  • В обоих случаях на основе гипотетического конструктора C(C) добавляется дополнительный воображаемый шаблон функции, определяемый как показано выше, и называемый копирующим кандидатом при выводе типов.

Вывод типа аргумента шаблона и разрешение перегрузки выполняются для инициализация воображаемого объекта гипотетического классового типа, сигнатуры конструкторов которого удовлетворяют правилам вывода (исключая возвращаемый тип) с целью формирования множества перегрузок. Контекст, в котором выполнен вывод типа аргументов шаблона класса, предоставляет инициализатор, кроме случая, когда первая фаза инициализации списком (рассматривающая конструкторы, инициализирующие списком) пропускается, если список инициализаторов состоит из единственного выражения с типом (возможно cv-квалифицированным) U, где U - специализация C или класс, наследованный от специализации C.

Эти воображаемые конструкторы (fictional constructors) являются публичными членами данного гипотетический классового типа. Они являются explicit-конструкторами, если правило вывода было сгенерировано на основе an explicit-конструктора. Если разрешение перегрузки завершилось неуспехом, программа считается неправильной (ill-formed). Иначе, возвращаемый тип выбранной специализации шаблона функции F становится выведенной специализацией шаблона класса.

template<class T> struct UniquePtr { UniquePtr(T* t); };
UniquePtr dp{new auto(2.0)};
// Единственный объявленный конструктор:
// C1: UniquePtr(T*);
// Множество неявно сгенерированных правила вывода типов:
// F1: template<class T> UniquePtr<T> F(T *p);
// F2: template<class T> UniquePtr<T> F(UniquePtr<T>); // копирующий кандидат при выводе
// воображаемый класс инициализатора:
// struct X {
//     template<class T> X(T *p);          // F1
//     template<class T> X(UniquePtr<T>);  // F2
// };
// прямая инициализация объекта типа X выражением "new double(2.0)"
// выбирает конструктор, соответствующий правилу F1 и T = double
// Для F1 и T = double, возвращаемым типом станет UniquePtr<double>
// Результат:
// UniquePtr<double> dp{new auto(2.0)}

Или более сложный случай (замечание: "S::N" не будет скомпилировано, поскольку квалификаторы области видимости не могут быть выведены):

template<class T> struct S {
  template<class U> struct N {
    N(T);
    N(T, U);
    template<class V> N(V, U);
  };
};
 
S<int>::N x{2.0, 1};
// неявно сгенерированными правилами вывода типов будут (заметьте, что T уже выведено как int):
// F1: template<class U> S<int>::N<U> F(int);
// F2: template<class U> S<int>::N<U> F(int, U);
// F3: template<class U, class V> S<int>::N<U> F(V, U);
// F4: template<class U> S<int>::N<U> F(S<int>::N<U>); (копирующий кандидат при выводе)
// Разрешение перегрузки для прямой инициализации списком выражением "{2.0, 1}"
// выберет F3 и U = int, V = double.
// Возвращаемый тип - S<int>::N<int>
// Результат:
// S<int>::N<int> x{2.0, 1};

[править] Правила вывода типов, определённые пользователем

Определённое пользователем правило вывода типов имеет синтаксис объявления функции с завершающим возвращаемым типом (trailing return type), за исключением того, что в качестве имени функции используется имя шаблона класса:

explicit(необязательно) имя-шаблона ( объявление-параметра ) -> простой-идентификатор-шаблона ;

Определённые пользователем правила вывода типов должны именовать шаблон класса и должны быть введены в ту же семантическую область видимости шаблона класса (в качестве которой могут выступать пространство имён или объемлющий класс) и, для шаблона класса-члена, должны иметь тот же уровень доступа, однако правила вывода типов не становятся членами этой области видимости.

Правило вывода типов это не функция и у него нет тела. При выводе типов аргументов шаблона класса правила вывода типов не участвуют в поиске имени (name lookup) и в разрешении перегрузки, за исключением разрешения перегрузки относительно других правил вывода типов. Правила вывода типов не могут быть повторно объявлены в одной и той же единице трансляции для одного и того же шаблона класса.

// объявление шаблона
template<class T> struct container {
    container(T t) {}
    template<class Iter> container(Iter beg, Iter end);
};
// дополнительное правило вывода типов
template<class Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;
// использует
container c(7); // OK: T выводится как int с помощью неявно сгенерированного правила
std::vector<double> v = { /* ... */};
auto d = container(v.begin(), v.end()); // OK: T выводится как double
container e{5, 6}; // ошибка: типа std::iterator_traits<int>::value_type не существует

Описанные выше воображаемые конструкторы для целей разрешения перегрузки являются explicit, если они соответствуют неявно сгенерированному правилу вывода типов, созданному на основе explicit-конструктора, или определённому пользователем правилу вывода типов, которое использует ключевое слово explicit. Как и всегда, такие конструкторы игнорируются в контексте инициализации копированием:

template<class T> struct A {
    explicit A(const T&, ...) noexcept; // #1
    A(T&&, ...);                        // #2
};
 
int i;
A a1 = { i, i }; // ошибка: не может быть выведено из rvalue-ссылки в #2,
                 // и #1 explicit, и не рассматривается при инициализации копированием.
A a2{i, i};      // OK, инициализация A<int>::#1
A a3{0, i};      // OK, инициализация A<int>::#2
A a4 = {0, i};   // OK, инициализация A<int>::#2
 
template<class T> A(const T&, const T&) -> A<T&>; // #3
template<class T> explicit A(T&&, T&&)  -> A<T>;  // #4
 
A a5 = {0, 1};   // ошибка: #3 выводится как A<int&>
                     // и #1 & #2 result in same parameter конструкторы.
A a6{0,1};       // OK, #4 выводится как A<int> и #2 initializes
A a7 = {0, i};   // ошибка: #3 выводится как A<int&>
A a8{0,i};       // ошибка: #3 выводится как A<int&>

Использование typedef-члена или псевдонима шаблона в конструкторе или списке параметров шаблонного конструктора как таковое не делает соответствующий параметр неявно сгенерированного правила невыводимым контекстом.

template<class T> struct B {
    template<class U> using TA = T;
    template<class U> B(U, TA<U>);  //#1
};
 
// Неявное правило вывода типов, сгенерированное из #1, эквивалентно
// template<class T, class U> B(U, T) -> B<T>;
// , а не
// template<class T, class U> B(U, typename B<T>::template TA<U>) -> B<T>;
// что было бы невыводимо
 
B b{(int*)0, (char*)0}; // OK, выводится как B<char*>

[править] Замечания

Вывод типа аргументов шаблона класса выполняется, только если не задан список аргументов. Если задан список аргументов шаблона, то вывод типа не производится.

std::tuple t1(1, 2, 3);              // OK: вывод типа
std::tuple<int,int,int> t2(1, 2, 3); // OK: заданы все аргументы
std::tuple<> t3(1, 2, 3);            // Ошибка: tuple<> не содержит подходящего конструктора
                                     //         Вывод типа не производится.
std::tuple<int> t4(1, 2, 3);         // Ошибка

Вывод типа аргументов шаблона класса-агрегата обычно требует наличия правил вывода типов:

template<class A, class B> struct Agg {A a; B b; };
// неявно сгенерированные правила вывода создаются из конструктора по умолчанию, конструктора копирования, и конструктора перемещения
template<class A, class B> Agg(A a, B b) -> Agg<A, B>;
Agg agg{1, 2.0}; // выводится как Agg<int, double> из определённого пользователем правила вывода
 
template <class... T>
array(T&&... t) -> array<std::common_type_t<T...>, sizeof...(T)>;
auto a = array{1, 2, 5u}; // выводится как array<unsigned, 3> из определённого пользователем правила вывода

Определённые пользователем правила вывода типов не обязательно должны быть шаблонами:

template<class T> struct S { S(T); };
S(char const*) -> S<std::string>;
S s{"hello"}; // выводится как S<std::string>

В рамках области видимости шаблона класса, имя шаблона без списка параметров является инъектированным именем класса, и может быть использовано как тип. В этом случае вывода типа аргументов шаблона не производится и параметры шаблона должны быть указаны явно:

template<class T>
struct X {
  X(T) { }
  template<class Iter> X(Iter b, Iter e) { }
 
  template<class Iter>
  auto foo(Iter b, Iter e) { 
     return X(b, e); // нет вывода типа: X эквивалентно X<T>
  }
  template<class Iter>
  auto bar(Iter b, Iter e) { 
     return X<typename Iter::value_type>(b, e); // тип параметра должен быть указан явно
  }
  auto baz() { 
     return ::X(0); // ::X не является инъектированным именем класса и выводится как ::X<int>
  }
};

При разрешении перегрузки частичное упорядочение будет важнее того, что шаблон функции сгенерирован из правила: если шаблон функции, сгенерированный из конструктора, более специализирован чем тот, что сгенерирован из правила вывода типов, выбирается шаблон, сгенерированный из конструктора. Поскольку копирующий кандидат (copy deduction candidate) при выводе обычно более специализирован, чем конструктор-адаптер (wrapping constructor), это значит, что копирование, как правило, предпочитается адаптированию (wrapping).

template<class T> struct A {
    A(T, int*);     // #1
    A(A<T>&, int*); // #2
    enum { value };
};
template<class T, int N = T::value> A(T&&, int*) -> A<T>; //#3
 
A a{1,0}; // использует #1 для вывода A<int> и #1 для инициализации
A b{a,0}; // использует #2 (более специализирован, чем #3) для вывода A<int> и #2 для инициализации

Когда применяемые в начале правила разрешения конфликтов, включая частичное упорядочивание, не дают возможности различить две шаблонные функции-кандидата, применяются следующие правила:

  • Шаблон функции, сгенерированный из правила вывода, предпочитается правилу, неявно сгенерированному из конструктора или из шаблонного конструктора.
  • Копирующий кандидат при выводе предпочитается любым другим шаблонам функций, неявно сгенерированным из конструктора или из шаблонного конструктора.
  • Шаблон функции, неявно сгенерированный из нешаблонного конструктора, предпочитается шаблону функции, неявно сгенерированному из шаблонного конструктора.
template <class T> struct A {
    using value_type = T;
    A(value_type); // #1
    A(const A&); // #2
    A(T, T, int); // #3
    template<class U> 
    A(int, T, U); // #4
}; // A(A); #5, копирующий кандидат при выводе
 
A x (1, 2, 3); // использует #3, сгенерированный из нешаблонного конструктора
 
template <class T> A(T) -> A<T>;  // #6 менее специализирован, чем #5
 
A a (42); // использует #6 для вывода A<int> и #1 для инициализации
A b = a;  // использует #5 для вывода A<int> и #2 для инициализации
 
template <class T> A(A<T>) -> A<A<T>>;  // #7 равно специализирован с #5
 
A b2 = a;  // использует #7 для вывода A<A<int>> и #1 для инициализации

Rvalue-ссылка на не cv-квалифицированный параметр шаблона не является универсальной ссылкой если этот параметр - параметр шаблона класса:

template<class T> struct A {
    template<class U>
    A(T&&, U&&, int*);   // #1: T&& - не универсальная ссылка
                         //     U&& - универсальная ссылка
    A(T&&, int*); // #2: T&& - не универсальная ссылка
};
 
template<class T> A(T&&, int*) -> A<T>; // #3: T&& - универсальная ссылка
 
int i, *ip;
A a{i, 0, ip};  // ошибка, невозможно вывести из #1
A a0{0, 0, ip}; // использует #1 для вывода A<int> и #1 для инициализации
A a2{i, ip};    // использует #3 для вывода A<int&> и #2 для инициализации

При инициализации одним аргументом с типом, являющимся специализацией этого же шаблона класса, по умолчанию выбирается копирующий вариант вывода:

std::tuple t1{1};   //std::tuple<int>
std::tuple t2{t1};  //std::tuple<int>, а не std::tuple<std::tuple<int>>
 
std::vector v1{1, 2};   // std::vector<int>
std::vector v2{v1};     // std::vector<int>, а не std::vector<std::vector<int>> (P0702R1)
std::vector v3{v1, v2}; // std::vector<std::vector<int>>

В случае инициализация списком и при наличии конструкторов, инициализирующих списком, правило предпочтения именно таких конструкторов остаётся прежним.

std::vector v1{1, 2}; // std::vector<int>
 
std::vector v2(v1.begin(), v1.end());  // std::vector<int>
std::vector v3{v1.begin(), v1.end()};  // std::vector<std::vector<int>::iterator>

До появления вывода типов аргументов шаблона класса, для того, чтобы избежать явного указания аргументов, частым решением было использование шаблона функции:

std::tuple p1{1, 1.0};             //std::tuple<int, double>, используется вывод типа
auto p2 = std::make_tuple(1, 1.0); //std::tuple<int, double>, до C++17

[править] Отчеты о дефектах

Следующие изменения поведения были применены с обратной силой к ранее опубликованным стандартам C++:

Номер Применён Поведение в стандарте Корректное поведение
WG не указан C++17 an initializer-list constructor can pre-empt the copy deduction candidate, resulting in wrapping initializer-list phase skipped when copying