До 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. Механіка: як компілятор це робить під капотом
Коли компілятор бачить створення об'єкта шаблону класу без явних кутових дужок <...>:
- Він бере кожен існуючий конструктор класу (включно з конструкторами копіювання/переміщення).
- Синтезує з кожного конструктора уявну фіктивну шаблонну функцію — implicit deduction guide (неявний гайд дедукції).
- Додає до цього списку явні гайди (explicit deduction guides), написані програмістом або стандартною бібліотекою.
- Проводить стандартний перевантажувальний аналіз (Overload Resolution) для цих функцій.
- На основі функції-переможця генерує цільовий тип класу.
Приклад генерації неявного гайду:
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
- Немає магії: CTAD — це просто автоматичне створення шаблонів функцій із ваших конструкторів та виклик звичайного Overload Resolution.
- Якщо конструктор приймає
Tза значенням: спрацьовує decay (масиви стають покажчиками, рядкові літерали типу"abc"виведутьconst char*, а не масив символів). - Якщо тип аргументу конструктора відрізняється від параметра шаблону: напишіть власний
MyType(...) -> MyType<...>;. - У C++20 сміливо використовуйте CTAD для звичайних
structбез конструкторів (агрегатів) та дляusing-аліасів.