Выбрать главу

auto z = {1, 2, 3.0};  // ошибка: нельзя вывести тип элемента 

Как это работает

Отдельно от механизма выведения типа auto оператор {} ведет себя предсказуемо, по крайней мере при инициализации обычных типов. При инициализации контейнеров наподобие std::vector, std::list и т.д. инициализатор с фигурными скобками будет соответствовать конструктору std::initializer_list этого класса-контейнера. При этом он не может соответствовать неагрегированным конструкторам (таковыми являются обычные конструкторы, в отличие от тех, что принимают список инициализаторов).

std::vector, например, предоставляет конкретный неагрегированный конструктор, заносящий в некоторое количество элементов одно и то же значение: std::vector<int> v (N, value). При записи std::vector<int> v {N, value} выбирается конструктор initializer_list, инициализирующий вектор с двумя элементами: N и value. Об этом следует помнить.

Есть интересное различие между оператором {} и вызовом конструктора с помощью обычных скобок (). В первом случае не выполняется неявных преобразований типа: int x (1.2); и int x = 1.2; инициализируют переменную x значением 1, округлив в нижнюю сторону число с плавающей точкой и преобразовав его к типу int. А вот выражение int x {1.2}; не скомпилируется, поскольку должно точно соответствовать типу конструктора.

  Кто-то может поспорить о том, какой стиль инициализации является лучшим. Любители стиля с фигурными скобками говорят, что последние делают процесс явным, переменная инициализируется при вызове конструктора и эта строка кода ничего не инициализирует повторно. Более того, при использовании фигурных скобок {} будет выбран единственный подходящий конструктор, в то время как в момент применения обычных скобок () — ближайший похожий конструктор, а также выполнится преобразование типов.

Дополнительное правило, включенное в С++17, касается инициализации с выведением типа auto: несмотря на то что в C++11 тип переменной auto x{123}; (std::initializer_list<int> с одним элементом) будет определен корректно, скорее всего, это не тот тип, который нужен. В С++17 та же переменная будет типа int.

Основные правила:

□ в конструкции auto var_name {one_element}; переменная var_name будет иметь тот же тип, что и one_element;

□ конструкция auto var_name {element1, element2,}; недействительна и не будет скомпилирована;

□ конструкция auto var_name = {element1, element2,}; будет иметь тип std::initializer_list<T>, где T — тип всех элементов списка.

В С++17 гораздо сложнее случайно определить список инициализаторов.

  Попытка скомпилировать эти примеры в разных компиляторах в режиме C++11 или C++14 покажет, что одни компиляторы автоматически выводят тип auto x {123}; как int, а другие — как std::initializer_list<int>. Подобный код может вызвать проблемы с переносимостью!

Разрешаем конструктору автоматически выводить полученный тип класса шаблона

Многие классы C++ обычно специализируются по типам, о чем легко догадаться по типам переменных, которые пользователь задействует при вызовах конструктора. Тем не менее до С++17 эти возможности не были стандартизированы. С++17 позволяет компилятору автоматически вывести типы шаблонов из вызовов конструктора.

Как это делается

Данную особенность очень удобно проиллюстрировать на примере создания экземпляров типа std::pair и std::tuple. Это можно сделать за один шаг:

std::pair my_pair (123, "abc");         // std::pair<int, const char*>

std::tuple my_tuple (123, 12.3, "abc"); // std::tuple<int, double, const char*>

Как это работает

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

template <typename T1, typename T2, typename T3>

class my_wrapper {

  T1 t1;

  T2 t2;

  T3 t3;

public:

  explicit my_wrapper(T1 t1_, T2 t2_, T3 t3_)

    : t1{t1_}, t2{t2_}, t3{t3_}

  {}

  /* … */

};

О’кей, это всего лишь еще один класс шаблона. Вот как мы раньше создавали его объект (инстанцировали шаблон):

my_wrapper<int, double, const char *> wrapper {123, 1.23, "abc"};

Теперь же можно опустить специализацию шаблона:

my_wrapper wrapper {123, 1.23, "abc"};

До появления C++17 это было возможно только при реализации вспомогательной функции: