C++17 STL Стандартная библиотека шаблонов — страница 6 из 119

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
, принимает произвольное количество параметров и суммирует их с помощью выражений свертки (пример, связанный с выражениями свертки, мы рассмотрим далее в этой главе). Полученная сумма сохраняется в переменную-член