От явных аргументов шаблона к CTAD

До C++17 аргументы шаблона класса обычно приходилось указывать явно:

Stack<int> stack;

Начиная с C++17 компилятор часто может вывести эти аргументы из инициализатора:

Stack stack{42}; // Stack<int>

Этот механизм называется Class Template Argument Deduction, или CTAD. Он применяется, когда имя шаблона класса используется без списка аргументов, а доступные кандидаты вывода содержат достаточно информации для определения всех обязательных аргументов.

Вывод из конструктора

Рассмотрим стек, который хранит элементы в std::vector и может быть инициализирован одним значением:

#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    Stack() = default;

    Stack(const T& elem)
        : elems{elem}
    {
    }

    void push(const T& elem)
    {
        elems.push_back(elem);
    }

    const T& top() const
    {
        return elems.back();
    }
};

Параметр конструктора содержит достаточно информации для вывода T:

Stack stack{42}; // T = int, тип Stack<int>

Отдельного внимания заслуживает явно заданный конструктор по умолчанию:

Stack() = default;

После объявления другого конструктора компилятор больше не объявляет обычный конструктор по умолчанию автоматически. Запись = default запрашивает его явно. Однако такой конструктор не помогает CTAD, потому что его список параметров не содержит информации, из которой можно вывести T:

Stack<int> first; // корректно: специализация известна
Stack second;     // ошибка: T невозможно вывести

Почему фигурные скобки имеют значение

В инициализаторе поля используются фигурные скобки:

Stack(const T& elem)
    : elems{elem}
{
}

При T = int выражение elems{elem} инициализирует вектор списком из одного элемента. Запись elems(elem) вместо этого могла бы выбрать конструктор вектора, который интерпретирует целое число как размер:

std::vector<int> oneElement{5};  // один элемент со значением 5
std::vector<int> fiveElements(5); // пять элементов, инициализированных значением по умолчанию

Как стандарт описывает CTAD

Стандарт C++ описывает CTAD с помощью набора воображаемых функций, называемых guides. Для каждого конструктора шаблона класса компилятор формирует кандидат с тем же списком параметров и возвращаемым типом, обозначающим соответствующую специализацию класса.

Для объяснения можно представить, что конструктор

template<typename T>
class Stack
{
public:
    Stack(const T&);
};

создаёт кандидат, похожий на следующую функцию:

// Только для объяснения: эта функция в программе не объявляется.
template<typename T>
auto F(const T&) -> Stack<T>;

Для инициализатора 42 вывод аргументов шаблона даёт T = int. Следовательно, возвращаемым типом выбранного кандидата становится Stack<int>.

Стандарт указывает, что placeholder шаблона класса заменяется возвращаемым типом, выбранным посредством overload resolution для вывода аргументов шаблона класса. Концептуально объявление можно прочитать следующим образом:

// Записано программистом:
Stack stack{42};

// Тип выбран посредством CTAD:
Stack<int> stack{42};

После определения специализации объявление всё ещё должно успешно инициализировать объект полученного типа. Кандидат, сформированный из конструктора, может одновременно определить специализацию и указать конструктор, который выполнит инициализацию. Пользовательский deduction guide, напротив, может определить специализацию, тогда как объект будет инициализирован другим, настоящим конструктором.

Эта модель описана в разделах [dcl.type.class.deduct] и [over.match.class.deduct]. Она определяет семантику языка, а не обязательную последовательность внутренних проходов компилятора.

Строковые литералы: ссылка и передача по значению

Строковые литералы демонстрируют важную особенность вывода аргументов шаблона. Литерал "hello" имеет тип массива const char[6]: пять символов и завершающий нулевой символ.

Когда конструктор принимает аргумент по ссылке, массивный тип сохраняется:

Stack(const T& elem);

Для следующего объявления вывод даёт T = char[6]. Квалификатор const задаётся шаблоном параметра const T&:

Stack stack{"hello"}; // попытка сформировать Stack<char[6]>

Эта специализация не подходит для показанной реализации. Обычный массив не удовлетворяет требованиям, предъявляемым к стандартному элементу std::vector, поэтому попытка инстанцировать std::vector<char[6]> делает объявление некорректным.

Одно из возможных решений — принимать аргумент конструктора по значению:

#include <utility>
#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    Stack() = default;

    Stack(T elem)
        : elems{std::move(elem)}
    {
    }
};

При выводе для параметра, принимаемого по значению, аргумент-массив проходит стандартное преобразование массива в указатель:

Stack stack{"hello"}; // Stack<const char*>

Теперь объявление корректно, но стек хранит указатели, а не строки. Он не владеет данными, на которые указывают эти указатели. Хотя строковый литерал имеет статическую продолжительность хранения, тот же тип стека впоследствии может получить указатели на буферы с меньшим временем жизни.

Deduction guide для строк с владением данными

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

Stack(const char*) -> Stack<std::string>;

Список параметров слева участвует в overload resolution. Тип справа является специализацией, которая выбирается при победе этого кандидата.

Полный пример с владеющими строками

#include <string>
#include <utility>
#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    Stack() = default;

    Stack(T elem)
        : elems{std::move(elem)}
    {
    }

    const T& top() const
    {
        return elems.back();
    }
};

Stack(const char*) -> Stack<std::string>;

int main()
{
    Stack integers{42};  // Stack<int>
    Stack text{"hello"}; // Stack<std::string>
}

Для text deduction guide выбирает Stack<std::string>. Затем компилятор инициализирует объект этой специализации, вызывая её конструктор. Преобразование "hello" в std::string происходит при инициализации параметра конструктора, а не при применении deduction guide.

Различие становится заметным при использовании разных форм инициализации:

Stack invalid = "hello"; // вывод успешен, инициализация завершается ошибкой
Stack valid{"hello"};    // Stack<std::string>

Для invalid deduction guide всё ещё может определить Stack<std::string>. Однако copy-initialization потребовала бы двух пользовательских преобразований в одной последовательности:

const char* -> std::string -> Stack<std::string>

Такая последовательность недопустима. При прямой инициализации списком компилятор непосредственно выбирает конструктор Stack<std::string>, поэтому требуется только преобразование аргумента в std::string.

Вывод при копировании

В набор кандидатов CTAD входит специальный copy deduction candidate. Он позволяет вывести специализацию из другого объекта того же шаблона класса:

Stack first{"hello"}; // Stack<std::string>

Stack second{first};    // Stack<std::string>
Stack third(first);     // Stack<std::string>
Stack fourth = {first}; // Stack<std::string>

Тип first уже содержит аргумент шаблона std::string. Поэтому copy deduction candidate выбирает ту же специализацию для нового объекта. При этом выбранный объект по-прежнему должен допускать копирование или другую соответствующую форму инициализации.

Существование этого кандидата определено в разделе [over.match.class.deduct] вместе с кандидатами, сформированными из конструкторов и пользовательских deduction guides.

Шаблонные агрегаты: C++17 и C++20

Шаблоны классов также могут быть агрегатами. Рассмотрим простую структуру, которая хранит значение и комментарий:

#include <string>

template<typename T>
struct ValueWithComment
{
    T value;
    std::string comment;
};

Правила CTAD для агрегатов различаются между C++17 и C++20:

Стандарт Вывод для этого агрегата
C++17 Требуются явные аргументы шаблона или пользовательский deduction guide.
C++20 Компилятор может сформировать aggregate deduction candidate из элементов агрегата.

В C++17 недостающее правило вывода можно задать явно:

template<typename T>
ValueWithComment(T, const char*)
    -> ValueWithComment<T>;

ValueWithComment item{42, "initial value"};
// ValueWithComment<int>

Deduction guide выбирает ValueWithComment<int>. После этого два поля полученной специализации инициализируются значениями 42 и "initial value".

Deduction guide также может намеренно изменить тип, выбранный для аргумента:

ValueWithComment(const char*, const char*)
    -> ValueWithComment<std::string>;

ValueWithComment item{"hello", "initial value"};
// ValueWithComment<std::string>

Начиная с C++20 простой пример с целым числом может использовать автоматический вывод для агрегата:

ValueWithComment item{42, "initial value"};
// C++20: ValueWithComment<int>

Компилятор формирует aggregate deduction candidate, когда класс удовлетворяет соответствующим требованиям к агрегату, инициализатор подходит для такого вывода и для шаблона класса не объявлены пользовательские deduction guides. Для более сложных агрегатов могут действовать дополнительные ограничения, связанные с базовыми классами, массивами и пакетами параметров.

Что нужно запомнить

  • CTAD появился в C++17 и позволяет выбрать специализацию шаблона класса по инициализатору.
  • Конструкторы создают неявные кандидаты вывода с соответствующими списками параметров.
  • Возвращаемый тип кандидата, выбранного посредством overload resolution, заменяет placeholder шаблона класса.
  • После вывода выбранная специализация всё ещё должна быть успешно инициализирована.
  • Deduction guide выбирает специализацию, но не является конструктором и сам не выполняет преобразования.
  • Параметры-ссылки сохраняют массивный тип строковых литералов, а при передаче по значению массив преобразуется в указатель.
  • Deduction guide может сопоставить const char* с std::string, если контейнер должен владеть текстом.
  • Copy deduction candidate переносит аргументы шаблона из другого объекта того же шаблона класса.
  • В C++17 для CTAD агрегата требуется пользовательский guide, а в C++20 для подходящих агрегатов может формироваться автоматический aggregate deduction candidate.