auto
, был высок шанс выразить не то, что вам нужно. В C++17 появился улучшенный набор правил инициализатора. В следующем примере вы увидите, как грамотно инициализировать переменные в С++17 и какой синтаксис при этом использовать.
Как это делается
Переменные инициализируются в один прием. При использовании синтаксиса инициализатора могут возникнуть две разные ситуации.
1. Применение синтаксиса инициализатора с фигурными скобками без выведения типа
auto
:
// Три идентичных способа инициализировать переменную типа int:
int x1 = 1;
int x2 {1};
int x3 (1);
std::vector v1 {1, 2, 3};
// Вектор, содержащий три переменные типа int: 1, 2, 3
std::vector v2 = {1, 2, 3};
// Такой же вектор
std::vector v3 (10, 20);
// Вектор, содержащий десять переменных типа int,
// каждая из которых имеет значение 20
2. Использование синтаксиса инициализатора с фигурными скобками с выведением типа
auto
:
auto v {1}; // v имеет тип int
auto w {1, 2}; // ошибка: при автоматическом выведении типа
// непосредственная инициализация разрешена
// только одиночными элементами! (нововведение)
auto x = {1}; // x имеет тип std::initializer_list
auto y = {1, 2}; // y имеет тип std::initializer_list
auto z = {1, 2, 3.0}; // ошибка: нельзя вывести тип элемента
Как это работает
Отдельно от механизма выведения типа
auto
оператор {}
ведет себя предсказуемо, по крайней мере при инициализации обычных типов. При инициализации контейнеров наподобие std::vector
, std::list
и т.д. инициализатор с фигурными скобками будет соответствовать конструктору std::initializer_list
этого класса-контейнера. При этом он не может соответствовать неагрегированным конструкторам (таковыми являются обычные конструкторы, в отличие от тех, что принимают список инициализаторов).std::vector
, например, предоставляет конкретный неагрегированный конструктор, заносящий в некоторое количество элементов одно и то же значение: std::vector v (N, value)
. При записи std::vector 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
с одним элементом) будет определен корректно, скорее всего, это не тот тип, который нужен. В С++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
— тип всех элементов списка.
В С++17 гораздо сложнее случайно определить список инициализаторов.
Попытка скомпилировать эти примеры в разных компиляторах в режиме C++11 или C++14 покажет, что одни компиляторы автоматически выводят тип
auto x {123};
как int
, а другие — как std::initializer_list
. Подобный код может вызвать проблемы с переносимостью!Разрешаем конструктору автоматически выводить полученный тип класса шаблона
Многие классы C++ обычно специализируются по типам, о чем легко догадаться по типам переменных, которые пользователь задействует при вызовах конструктора. Тем не менее до С++17 эти возможности не были стандартизированы. С++17 позволяет компилятору автоматически вывести типы шаблонов из вызовов конструктора.
Как это делается
Данную особенность очень удобно проиллюстрировать на примере создания экземпляров типа
std::pair
и std::tuple
. Это можно сделать за один шаг:
std::pair my_pair (123, "abc"); // std::pair
std::tuple my_tuple (123, 12.3, "abc"); // std::tuple
Как это работает
Определим класс-пример, где автоматическое выведение типа шаблона будет выполняться на основе переданных значений:
template
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 wrapper {123, 1.23, "abc"};
Теперь же можно опустить специализацию шаблона:
my_wrapper wrapper {123, 1.23, "abc"};
До появления C++17 это было возможно только при реализации вспомогательной функции:
my_wrapper make_wrapper(T1 t1, T2 t2, T3 t3)
{
return {t1, t2, t3};
}
Используя подобные вспомогательные функции, можно было добиться такого же эффекта:
auto wrapper (make_wrapper(123, 1.23, "abc"));
STL предоставляет множество аналогичных инструментов:
std::make_shared
, std::make_unique
, std::make_tuple
и т.д. В C++17 эти функции могут считаться устаревшими. Но, конечно, они все еще будут работать для обеспечения обратной совместимости.
Дополнительная информация
Из данного примера мы узнали о неявном выведении типа шаблона. Однако в некоторых случаях на этот способ нельзя полагаться. Рассмотрим следующий класс-пример:
template
struct sum {
T value;
template
sum(Ts&& ... values) : value{(values + ...)} {}
};
Эта структура,
sum
, принимает произвольное количество параметров и суммирует их с помощью выражений свертки (пример, связанный с выражениями свертки, мы рассмотрим далее в этой главе). Полученная сумма сохраняется в переменную-член