До C++17 шаблони класів не вміли виводити свої параметри з аргументів конструктора. Доводилося або писати громіздкий код, або створювати допоміжні фабричні функції (make_pair, make_tuple тощо):

// До C++17:
std::pair<int, double> p1(42, 3.14);
auto p2 = std::make_pair(42, 3.14); // трюк: шаблони функцій виводити типи вміли завжди

// Починаючи з C++17 (CTAD):
std::pair p(42, 3.14); // компілятор сам виводить std::pair<int, double>

CTAD (Class Template Argument Deduction) — це механізм, який транслює створення об'єкта класу в уже знайоме компілятору виведення типів для виклику функції.


1. Механіка: як компілятор це робить під капотом

Коли компілятор бачить створення об'єкта шаблону класу без явних кутових дужок <...>:

  1. Він бере кожен існуючий конструктор класу (включно з конструкторами копіювання/переміщення).
  2. Синтезує з кожного конструктора уявну фіктивну шаблонну функцію — implicit deduction guide (неявний гайд дедукції).
  3. Додає до цього списку явні гайди (explicit deduction guides), написані програмістом або стандартною бібліотекою.
  4. Проводить стандартний перевантажувальний аналіз (Overload Resolution) для цих функцій.
  5. На основі функції-переможця генерує цільовий тип класу.

Приклад генерації неявного гайду:

template <typename T>
struct Holder {
    T value;
    Holder(T val); // конструктор
};

// Компілятор автоматично генерує уявну функцію-кандидата:
// template <typename T>
// Holder<T> __Holder_guide(T val);

Коли ви пишете Holder h(10);, компілятор підставляє аргумент 10 у синтезовану функцію, виводить T = int і створює тип Holder<int>.


2. Навіщо потрібні явні Deduction Guides (Гайди виведення)

Неявних гайдів вистачає не завжди. Є випадки, коли сигнатура конструктора не збігається з бажаними типами параметрів шаблону класу або конструктор приймає щось нетривіальне (наприклад, ітератори).

Синтаксис deduction guide виглядає як «функція без тіла зі стрілкою»:

template <...>
НазваКласу(Параметри) -> НазваКласу<ВиведеніТипи>;

Класичний приклад: конструктор по ітераторах (std::vector)

У std::vector конструктор приймає два ітератори:

template <typename T>
class vector {
public:
    template <typename InputIt>
    vector(InputIt first, InputIt last);
};

Якби не було explicit deduction guide, спроба написати:

std::vector v(iter_begin, iter_end);

призвела б до глухого кута: тип елементів T взагалі не фігурує в аргументах конструктора (там тільки InputIt). Компілятор не знає, що вектор має складатися з typename std::iterator_traits<InputIt>::value_type.

Тому в стандартній бібліотеці прописано явний deduction guide:

template <typename InputIt>
vector(InputIt, InputIt) 
    -> vector<typename std::iterator_traits<InputIt>::value_type>;

Тепер при виклику std::vector v(list.begin(), list.end()) компілятор дивиться на тип, на який вказує ітератор, і створює правильний вектор.


3. Болючий виняток: Захист від подвійного загортання (Vector of Vectors)

CTAD має спеціальне правило щодо конструкторів копіювання/переміщення, яке іноді призводить до неочевидної поведінки:

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

std::vector v2{v1};      // Що це: std::vector<int> чи std::vector<std::vector<int>>?
  • Відповідь: це std::vector<int> (копія v1), а не вектор векторів з одним елементом. Синтезований гайд конструктора копіювання завжди має найвищий пріоритет, якщо передано одиничний об'єкт того самого базового типу.

Але якщо передати кілька аргументів:

std::vector v3{v1, v1};  // std::vector<std::vector<int>> (бо тут спрацював список ініціалізації)

4. Що змінилося та покращилося в C++20

У C++17 CTAD мав серйозні обмеження, які виправили в C++20:

1. CTAD для Aggregate-типів (C++20)

У C++17 CTAD вимагав наявності хоча б одного явно оголошеного конструктора. Якщо тип був звичайним агрегатом (без конструкторів), CTAD не працював:

template <typename T, typename U>
struct Pair {
    T first;
    U second;
    // конструкторів нема — це чистий Aggregate
};

// У C++17: ПОМИЛКА КОМПІЛЯЦІЇ (треба було писати Pair<int, double>{1, 2.0})
// У C++20: ПРАЦЮЄ ЧУДОВО!
Pair p{1, 2.0}; // Pair<int, double>

У C++20 компілятор навчився автоматично синтезувати deduction guides для ініціалізації полів агрегатів.

2. CTAD за псевдонімами типів (Alias Templates — C++20)

У C++17 не можна було застосовувати CTAD через using:

template <typename T>
using StringMap = std::map<std::string, T>;

// C++17: помилка
// C++20: працює!
StringMap m{ {"apple", 1}, {"banana", 2} }; // виведе StringMap<int>

3. Обмеження концептами (Constraints)

Гайди тепер можна прямо обмежувати за допомогою requires або концептів:

template <std::integral T>
MyContainer(T) -> MyContainer<long long>;

Підсумкова шпаргалка з правил CTAD

  1. Немає магії: CTAD — це просто автоматичне створення шаблонів функцій із ваших конструкторів та виклик звичайного Overload Resolution.
  2. Якщо конструктор приймає T за значенням: спрацьовує decay (масиви стають покажчиками, рядкові літерали типу "abc" виведуть const char*, а не масив символів).
  3. Якщо тип аргументу конструктора відрізняється від параметра шаблону: напишіть власний MyType(...) -> MyType<...>;.
  4. У C++20 сміливо використовуйте CTAD для звичайних struct без конструкторів (агрегатів) та для using-аліасів.