При добавлении пользовательского конструктора к классу сохраняется ли возможность агрегатной инициализации?
Обычно нет: пользовательский конструктор меняет статус класса, и он перестаёт быть агрегатом. В результате фигурная инициализация начинает выбирать конструктор, а не напрямую инициализировать базовые классы и нестатические поля по правилам агрегата.
Точная граница зависит от стандарта. В C++17 агрегат не должен иметь пользовательских, явных или унаследованных конструкторов; в C++20 ключевым ограничением стало отсутствие объявленных или унаследованных конструкторов.
Агрегатная инициализация появилась как способ удобно и предсказуемо инициализировать простые структуры без написания конструкторов. Она сохраняет близкую к структурам C модель: значения последовательно сопоставляются базовым классам и нестатическим членам объекта.
По мере развития C++ правила уточнялись. В частности, стандарт постепенно разрешал больше возможностей внутри агрегатов, например инициализаторы членов, но наличие конструктора по-прежнему отделяет класс с конструкторной логикой от простого набора данных.
Изменение класса может выглядеть безобидно: разработчик добавляет конструктор для проверки аргументов или задания значения по умолчанию. Однако прежние места с фигурной инициализацией могут начать означать вызов конструктора вместо агрегатной инициализации.
Это приводит к нескольким последствиям: часть вызовов перестаёт компилироваться, порядок и правила инициализации меняются, а добавление конструктора становится потенциально ломающим изменением интерфейса типа.
У агрегата фигурная инициализация сопоставляет элементы объекта с переданными значениями в определённом порядке. Конструктор при этом не вызывается, потому что у агрегата в рассматриваемом смысле нет подходящей конструкторной логики.
После добавления пользовательского конструктора фигурная запись всё ещё может быть синтаксически допустимой, но её смысл меняется: компилятор пытается вызвать этот конструктор. Если подходящей сигнатуры нет, программа не компилируется.
Для агрегатной инициализации важны и другие ограничения: например, отсутствие виртуальных функций и недоступных нестатических полей. Поэтому удаление конструктора не всегда достаточно, чтобы тип снова стал агрегатом.
В C++20 также появились обозначенные инициализаторы, но они применяются только к агрегатам и должны указывать нестатические поля в порядке их объявления. Добавление конструктора лишает класс и этой возможности.
Следует отличать наличие пользовательского конструктора от наличия неявно генерируемого конструктора. Компилятор может генерировать специальные функции сам, и это само по себе не означает, что класс перестаёт быть агрегатом. Важен статус конструктора по правилам конкретного стандарта C++.
В библиотеке конфигурация сервиса была простым агрегатом: клиент создавал объект, задавая порт, режим отладки и тайм-аут. Такой интерфейс был удобен, но не гарантировал проверку диапазонов значений.
Рассматривались два варианта. Добавление конструктора позволяло централизовать проверку, но ломало часть агрегатной инициализации и делало структуру менее гибкой. Сохранение агрегата не меняло интерфейс, однако требовало проверять корректность отдельно.
Выбранным решением стала фабричная функция, которая принимает значения, создаёт агрегат и выполняет проверку. Это сохранило совместимость простого типа данных и предоставило безопасный путь создания объектов; при этом пользователи всё ещё могли осознанно создавать агрегат напрямую в низкоуровневом коде.
Нет. Фигурные скобки задают форму инициализации, но конкретный механизм зависит от типа. Для неагрегатного класса они могут выбрать конструктор, для агрегата — инициализировать его элементы, а для некоторых типов действуют специальные правила, например приоритет конструктора, принимающего std::initializer_list.
Не всегда. Инициализаторы нестатических полей сами по себе не обязательно запрещают агрегатный статус в современных стандартах, но должны выполняться все остальные требования агрегата. Кроме того, удаление конструктора может изменить доступность и поведение других форм создания объектов, поэтому результат нужно проверять по версии стандарта и фактическому определению класса.
Можно написать конструктор с соответствующей сигнатурой, но это уже будет вызов конструктора, а не агрегатная инициализация. Разница существенна: применяются правила выбора перегрузки, тело конструктора, его проверки и порядок инициализации членов. Поэтому совпадение синтаксиса не означает сохранение прежней семантики.