ядро языка — инициализацияcore language — initialization

designated initializers

Point p{.x = 1, .y = 2}; — инициализация полей агрегата по явному имени, а не по позиции в списке.Point p{.x = 1, .y = 2}; — initializing an aggregate's fields by explicit name instead of by position in the list.

часто используетсяcommonly used раньше — неформальное расширение GCC/Clangpreviously an informal GCC/Clang extension

Что этоWhat it is

Designated initializers позволяют инициализировать поля агрегата (структуры/класса без пользовательских конструкторов), явно указывая имя каждого поля: Point p{.x = 1, .y = 2};. Правила: имена полей должны идти строго в том же порядке, в каком они объявлены в структуре; поля можно пропускать — пропущенные инициализируются как при обычной агрегатной инициализации, значением по умолчанию; нельзя смешивать именованную и позиционную инициализацию в одном списке — либо всё по именам, либо всё по позициям.Designated initializers let you initialize an aggregate's fields (a struct/class with no user-defined constructors) by explicitly naming each one: Point p{.x = 1, .y = 2};. The rules: field names must appear in exactly the order they're declared in the struct; fields may be skipped — skipped ones are initialized the same way an ordinary aggregate initialization would, to a default value; named and positional initialization can't be mixed in the same list — it's either all names or all positions.

до C++20 — только позиционная инициализацияbefore C++20 — positional initialization only
struct Config {
    int width;
    int height;
    bool fullscreen;
};

Config c{1920, 1080, false};
// какое значение к какому полю относится?// which value belongs to which field?
// нужно смотреть определение структуры, чтобы понять// you have to check the struct's definition to know
C++20 — по именам полейC++20 — by field name
struct Config {
    int width;
    int height;
    bool fullscreen;
};

Config c{.width = 1920, .height = 1080, .fullscreen = false};
// очевидно без заглядывания в определение структуры// obvious without looking up the struct's definition

Зачем ввелиWhy it was introduced

Позиционная агрегатная инициализация была устойчивым источником молчаливых багов при рефакторинге:Positional aggregate initialization was a persistent source of silent bugs during refactoring:

01

Рефакторинг порядка полей молча ломал смысл существующего кодаRefactoring field order silently broke the meaning of existing code

Добавление нового поля в середину структуры или перестановка существующих полей заставляла весь код с позиционной инициализацией Point p{1, 2} продолжать компилироваться, но со сдвинутыми, неверными значениями — компилятор не выдаёт ошибку, потому что типы всё ещё совпадают, просто семантика молча поменялась.Adding a new field in the middle of a struct, or reordering existing ones, let all code using positional initialization like Point p{1, 2} keep compiling, but with shifted, wrong values — the compiler doesn't error out because the types still match; the semantics have just silently changed.

02

Позиционные списки нечитаемы при большом числе полейPositional lists are unreadable once a struct has many fields

Инициализация структуры с шестью-семью полями через один список значений {1920, 1080, false, true, 0, 60} вынуждает читателя держать в голове порядок объявления полей или постоянно сверяться с определением структуры — designated initializers делают привязку значения к полю видимой прямо в месте использования.Initializing a struct with six or seven fields via a single value list {1920, 1080, false, true, 0, 60} forces the reader to keep the field declaration order in their head, or constantly cross-check the struct's definition — designated initializers make the value-to-field mapping visible right at the point of use.

03

Формализация давно существовавшего компиляторного расширенияFormalizing a long-standing compiler extension

Designated initializers существовали в C с C99 и долгое время были неформальным расширением GCC/Clang в режиме C++ — код на этом расширении уже писался в реальных проектах без официальной поддержки стандартом. C++20 стандартизировал уже сложившуюся практику, а не изобрёл её с нуля.Designated initializers had existed in C since C99, and for a long time were an informal GCC/Clang extension in C++ mode — code using this extension was already being written in real projects without official standard support. C++20 standardized an already-established practice rather than inventing it from scratch.

Как это работает под капотомHow it works under the hood

Designated initializers — это чисто синтаксический механизм этапа компиляции: компилятор сопоставляет каждое .имя = значение с полем структуры по его позиции объявления, затем выполняет обычную агрегатную инициализацию, как если бы список был позиционным. Пропущенные поля инициализируются так же, как при обычной агрегатной инициализации с неполным списком — значением по умолчанию для их типа (для скалярных типов — нулевая инициализация, если не указан инициализатор по умолчанию в самой структуре).Designated initializers are a purely compile-time syntactic mechanism: the compiler matches each .name = value to the struct's field by its declaration position, then performs ordinary aggregate initialization as if the list were positional. Skipped fields are initialized the same way an ordinary aggregate initialization with a shorter list would handle them — to their type's default value (zero-initialization for scalar types, unless the struct itself supplies a default member initializer).

struct Config {
    int width;
    int height;
    bool fullscreen = false;   // default member initializer// default member initializer
    int refresh_rate = 60;
};

// пропуск полей — они получают значение по умолчанию:// skipping fields — they get their default value:
Config c1{.width = 1920, .height = 1080};
// fullscreen == false, refresh_rate == 60 — как и без designated initializer// fullscreen == false, refresh_rate == 60 — same as without a designated initializer

// ошибка компиляции — нарушен порядок объявления:// compile error — declaration order violated:
// Config c2{.height = 1080, .width = 1920};

// ошибка компиляции — смешаны именованная и позиционная формы:// compile error — named and positional forms mixed:
// Config c3{.width = 1920, 1080};

Designated initializers особенно полезны для структур с полем-структурой (вложенная агрегатная инициализация) и для конфигурационных структур с большим числом полей, где явно нужны лишь некоторые — остальные остаются на default member initializer, и явное имя поля документирует, какие значения вообще были осознанно заданы вызывающей стороной:Designated initializers are especially useful for structs containing a nested struct field (nested aggregate initialization) and for configuration structs with many fields where only a few actually need to be set explicitly — the rest fall back to their default member initializer, and the explicit field name documents which values the caller consciously chose to set at all.

struct Timeouts {
    int connect_ms = 1000;
    int read_ms = 5000;
};

struct HttpClientConfig {
    std::string base_url;
    Timeouts timeouts;         // поле-структура// a nested struct field
    int max_retries = 3;
    bool keep_alive = true;
};

// вложенная designated-инициализация — видно, что меняется именно read_ms:// nested designated initialization — it's clear that read_ms specifically changes:
HttpClientConfig cfg{
    .base_url = "https://api.example.com",
    .timeouts = { .read_ms = 30000 },   // connect_ms остаётся 1000 по умолчанию// connect_ms stays at its default of 1000
};
// max_retries == 3, keep_alive == true — не тронуты, читатель это видит по отсутствию имени// max_retries == 3, keep_alive == true — untouched, the reader sees this by the absence of the name

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Конфигурационная структура с 7 полями — инициализируются только 2-3 нужныхA configuration struct with 7 fields — only the 2-3 needed ones get initialized

Типичный сценарий для настроек сервиса — большинство вызывающих сторон меняют лишь пару параметров, остальные остаются на разумных default member initializer.A typical service-settings scenario — most callers change just a couple of parameters, the rest stay at sensible default member initializers.

struct ServerConfig {
    std::string host = "0.0.0.0";
    int port = 8080;
    int worker_threads = 4;
    int backlog = 128;
    bool tcp_nodelay = true;
    bool reuse_port = false;
    int shutdown_timeout_ms = 5000;
};

ServerConfig cfg{.port = 9000, .worker_threads = 16};
// остальные 5 полей — значения по умолчанию, явно видно, что менялось// the other 5 fields — default values, it's clear at a glance what changed
02

До/после для структуры сетевого запроса с множеством опциональных полейBefore/after for a network-request struct with many optional-like fields

Позиционная форма для структуры такого размера практически нечитаема без постоянной сверки с определением — designated initializer делает соответствие поле-значение очевидным.The positional form for a struct this size is practically unreadable without constantly checking the definition — the designated initializer makes the field-to-value mapping obvious.

struct Request {
    std::string url;
    std::string method = "GET";
    int timeout_ms = 3000;
    bool follow_redirects = true;
    int max_redirects = 5;
};

// до: Request r{"https://api.example.com", "POST", 3000, true, 5}; — что есть что?// before: Request r{"https://api.example.com", "POST", 3000, true, 5}; — which is which?
// после — очевидно без заглядывания в определение:// after — obvious without looking up the definition:
Request r{.url = "https://api.example.com", .method = "POST"};
03

Designated initializer вместе с полями std::optional<T>A designated initializer alongside std::optional<T> fields

Явная инициализация лишь тех optional-полей, которые реально нужны — пропущенные остаются std::nullopt, что для optional и есть значение по умолчанию.Explicit initialization of only the optional fields actually needed — the skipped ones stay std::nullopt, which is optional's default value.

struct UserFilter {
    std::optional<std::string> name_contains;
    std::optional<int> min_age;
    std::optional<int> max_age;
    std::optional<bool> is_active;
};

UserFilter f{.min_age = 18, .is_active = true};
// name_contains и max_age остаются std::nullopt// name_contains and max_age stay std::nullopt
04

Типичная ошибка при рефакторинге — порядок полей всё ещё важенA typical refactoring mistake — field order still matters

Named initializers защищают от подстановки значения не в то поле, но не отменяют требование указывать поля в порядке их объявления — нарушение ловится компилятором, но сам факт ошибки стоит держать в голове при перестановке полей структуры.Named initializers protect against a value landing in the wrong field, but they don't waive the requirement to list fields in declaration order — the violation is caught by the compiler, but the fact itself is worth keeping in mind when reordering a struct's fields.

struct Point {
    int x;
    int y;
};

// ошибка компиляции: y объявлен после x, но указан первым// compile error: y is declared after x, but named first
// Point p{.y = 1, .x = 2};
// "designator order does not match declaration order"// "designator order does not match declaration order"

Point p{.x = 2, .y = 1};   // корректно — порядок совпадает с объявлением// correct — order matches the declaration

ГраблиGotchas

C++20 строже, чем C99 и чем расширения GCC/Clang до стандартизацииC++20 is stricter than C99 and than the pre-standard GCC/Clang extensions

C99 разрешает произвольный порядок именованных полей в инициализаторе; GCC/Clang как расширение в режиме C++ тоже нередко это допускали неформально. Стандарт C++20 требует строгого соответствия порядку объявления полей — код, писавшийся под "compiler extension"-поведение GCC/Clang до C++20, может не скомпилироваться в строгом режиме C++20 из-за этого расхождения.C99 allows named fields in an initializer to appear in any order; GCC/Clang, as an extension in C++ mode, often permitted this informally too. The C++20 standard requires strict adherence to declaration order — code written against the pre-C++20 "compiler extension" behavior of GCC/Clang may fail to compile under strict C++20 mode because of this discrepancy.

Работает только с агрегатамиWorks only with aggregates

Designated initializers применимы только к агрегатам: структурам/классам без пользовательских конструкторов, без приватных или защищённых нестатических членов, без виртуальных функций, без базовых классов с не-агрегатными свойствами — те же ограничения, что действуют для structured bindings (см. ../cpp17/structured-bindings.html). Обычный класс с конструктором эту фичу использовать не может — для него нужен вызов конструктора с именованными параметрами через отдельный паттерн (например, builder), а не designated initializers.Designated initializers apply only to aggregates: structs/classes with no user-defined constructors, no private or protected non-static members, no virtual functions, no base classes with non-aggregate properties — the same restrictions that apply to structured bindings (see ../cpp17/structured-bindings.html). An ordinary class with a constructor can't use this feature — it needs a separate pattern (say, a builder) for named-parameter-style construction, not designated initializers.

Пропуск поля — это инициализация по умолчанию, а не "оставить как есть"Skipping a field means default-initializing it, not "leave as-is"

Пропущенное в списке поле не остаётся неинициализированным и не сохраняет какое-то "предыдущее" значение — оно инициализируется точно так же, как в обычной агрегатной инициализации с неполным списком: значением по умолчанию для своего типа или своим default member initializer, если он задан в структуре.A field omitted from the list isn't left uninitialized and doesn't retain some "previous" value — it's initialized exactly the way an ordinary aggregate initialization with a shorter list would: to its type's default value, or to its default member initializer if the struct provides one.