Инициализация в C++: способы, ловушки и что выбрать
Фигурные скобки, круглые, знак равенства — в C++ у инициализации минимум пять форм, и они не взаимозаменяемы. Разбираем, где какая работает, где narrowing спасёт от бага, а где most vexing parse съест ваш объект.

Строка std::vector<int> v(10, 5) создаёт вектор из десяти пятёрок. Строка std::vector<int> v{10, 5} — вектор из двух элементов, 10 и 5. Одна пара скобок вместо другой — и вы получаете совсем не тот объект. Это не редкий угловой случай, а повседневная ловушка C++. Разберём все основные формы инициализации, чем они отличаются на уровне выбора конструктора и правил сужения, и по какому критерию выбирать между ними. Ориентируемся на C++17 и новее, отдельно отмечаю поведение, изменившееся в C++11 и C++20.
Пять форм и то, что за ними стоит
Синтаксически различают несколько записей. За похожим внешним видом стоят разные правила разрешения перегрузок.
- Copy-инициализация:
T x = value; - Direct-инициализация:
T x(value); - List-инициализация (direct):
T x{value}; - List-инициализация (copy):
T x = {value}; - Value/default-инициализация:
T x{};илиT x;
Практическая разница между этими формами — в трёх вещах: какие конструкторы вообще рассматриваются, как выбирается конкретный из них и что происходит с сужающими преобразованиями типов.
Copy против direct: барьер explicit
Форма T x = value не вызывает конструкторы, помеченные explicit. Direct-форма T x(value) их вызывает. Отсюда классическое различие:
#include <fstream>
#include <string>
// std::ifstream имеет explicit-конструктор от const char*
std::ifstream file_ok("data.txt"); // работает: direct-инициализация
// std::ifstream file_bad = "data.txt"; // ошибка компиляции: конструктор explicit
// А вот у std::string конструктор от const char* не explicit
std::string name = "Anna"; // работает: copy-инициализация
std::string other("Boris"); // тоже работает
Правило простое: explicit — это запрет на неявное создание объекта из значения другого типа. Copy-форма считается неявной и потому упирается в этот запрет.
Фигурные скобки и narrowing
Главная причина, по которой C++11 ввёл фигурные скобки, — запрет сужающих преобразований (narrowing conversions). Внутри {} компилятор откажется молча урезать значение.
int a = 3.14; // компилируется, a == 3, дробная часть отброшена молча
int b(3.14); // тоже компилируется, b == 3
int c{3.14}; // ОШИБКА компиляции: narrowing из double в int
long big = 5000000000L;
// int d{big}; // ОШИБКА, если значение не влезает в int
char e{300}; // ОШИБКА: 300 не помещается в char (обычно)
Это единственная форма, которая ловит потерю данных на этапе компиляции. Для числовых типов, где важна корректность, {} предпочтительнее.
Фигурные скобки не про красоту синтаксиса. Это единственная форма инициализации, которая превращает молчаливую потерю точности в ошибку компиляции.
Ловушка std::initializer_list
Как только у типа есть конструктор, принимающий std::initializer_list, фигурные скобки начинают предпочитать именно его — даже если по количеству и типам аргументов лучше подошёл бы обычный конструктор.
#include <vector>
std::vector<int> v1(10, 5); // конструктор (count, value): 10 элементов, каждый == 5
std::vector<int> v2{10, 5}; // initializer_list: 2 элемента, 10 и 5
std::vector<int> v3(10); // 10 элементов, каждый == 0
std::vector<int> v4{10}; // 1 элемент со значением 10
Здесь {} не выручает, а вредит. Если вам нужен конкретный конструктор с числовыми аргументами (размер, повтор), используйте круглые скобки. Фигурные оставьте для случаев, когда вы действительно перечисляете элементы.
Пустые фигурные скобки — особый случай
Запись T x{} с пустыми скобками вызывает не конструктор от initializer_list, а конструктор по умолчанию. Пустой initializer_list сюда не подставляется.
std::vector<int> empty{}; // вызван конструктор по умолчанию, вектор пуст
int counter{}; // value-инициализация, counter == 0
double ratio{}; // ratio == 0.0
Most vexing parse — когда объявление съедает объект
Круглые скобки создают проблему на пустом месте. Компилятор в неоднозначной ситуации предпочитает толковать конструкцию как объявление функции, а не как создание объекта.
#include <string>
struct Widget {
Widget() {}
};
Widget w(); // НЕ объект! Это объявление функции w, возвращающей Widget
Widget w2{}; // объект: value-инициализация через фигурные скобки
// Более коварный вариант с аргументом-времянкой:
// std::string s(std::string()); // объявление функции s(std::string(*)())
std::string s{std::string{}}; // корректный объект
Именно most vexing parse — весомый аргумент за фигурные скобки: они однозначны, компилятор не может перепутать их с прототипом функции.
Что выбирать
Сводная картина по формам:
| Форма | Вызывает explicit | Ловит narrowing | Риск most vexing parse | Предпочитает initializer_list |
|---|---|---|---|---|
T x = v; | нет | нет | нет | — |
T x(v); | да | нет | да | нет |
T x{v}; | да | да | нет | да |
T x = {v}; | нет | да | нет | да |
T x{}; | да | — | нет | нет (конструктор по умолчанию) |
Практический критерий
- По умолчанию берите фигурные скобки — они запрещают narrowing и защищают от most vexing parse. Это рекомендация из C++ Core Guidelines (правило ES.23).
- Исключение — конструкторы с числовыми аргументами у контейнеров (размер, счётчик повторов). Здесь
{}уйдёт вinitializer_list. Используйте круглые скобки:std::vector<int> v(n). - Для
autoбудьте осторожны.auto x{5}в C++17 даётint, ноauto x = {5}даётstd::initializer_list<int>— это почти никогда не то, что нужно.
auto a = 5; // int
auto b{5}; // int (начиная с C++17; до этого был initializer_list)
auto c = {5}; // std::initializer_list<int> — редко нужно
// auto d{5, 6}; // ОШИБКА в C++17
Инициализация членов класса
Инициализация членов в списке инициализации конструктора и инициализация по умолчанию прямо в объявлении — отдельная тема, разворачивать её здесь не буду. Один момент, который путают: порядок инициализации членов определяется порядком их объявления в классе, а не порядком в списке инициализации конструктора. Многие компиляторы выдают предупреждение при несовпадении — включите -Wreorder в GCC и Clang.
Что делать дальше
Включите предупреждения, которые ловят перечисленные ловушки: -Wall -Wextra в GCC/Clang даёт -Wnarrowing и -Wreorder. Прогоните существующий код через clang-tidy с проверкой modernize-use-emplace и родственными — она подскажет места, где инициализация написана неоптимально. И держите под рукой раздел Initialization на cppreference: правила разрешения перегрузок для list-инициализации меняли формулировки между стандартами, и точное поведение проще свериться там, чем держать в голове.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: cppreference — Initialization, C++ Core Guidelines — ES.23: Prefer the {}-initializer syntax
Частые вопросы
У меня C++11, а не C++17. Что из статьи не сработает?
Почему vector<int>{10, 5} даёт два элемента, а не десять?
Так фигурные скобки всегда безопаснее? Можно писать только их?
Компилятор ругается на narrowing, но я уверен, что значение влезает. Что делать?
В чём разница между int x{} и int x; для локальной переменной?
Работает ли всё это одинаково в MSVC, GCC и Clang?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.