Разберите неоднозначную запись локальной сущности: по какому правилу компилятор выбирает между объектом и функцией?
В неоднозначных случаях синтаксический анализатор C++ предпочитает трактовку записи как объявления функции. Поэтому конструкция, визуально похожая на создание локального объекта без аргументов, может объявить функцию, возвращающую объект указанного типа. Для явного создания объекта используют фигурные скобки или другую однозначную форму инициализации.
Синтаксис C++ объединяет объявления объектов и функций в единую декларативную грамматику. Это позволяет описывать сложные типы, но создаёт ситуации, где одна и та же последовательность токенов формально подходит сразу под несколько трактовок.
Правило выбора декларативной трактовки возникло как часть грамматики языка: компилятор должен получить однозначное объявление, не угадывая намерение программиста. На практике это породило известную проблему most vexing parse — «самый досадный разбор».
Рассмотрим локальную запись Widget w();. По интуиции она может выглядеть как создание объекта w типа Widget с вызовом конструктора без аргументов. Однако синтаксически она является объявлением функции w, возвращающей Widget и не принимающей параметров.
Ошибка проявится не обязательно в месте объявления. Программа может успешно компилироваться, но попытка использовать w как объект приведёт к диагностике или к неожиданному выбору перегрузки. Особенно опасны такие записи в шаблонах, конструкторах и сложных декларациях типов.
В неоднозначной ситуации C++ выбирает декларативный разбор. Круглые скобки после имени сущности позволяют интерпретировать запись как объявление функции, поэтому Widget w(); не создаёт объект.
Минимальный пример:
x создаётся с value-initialization через пустые фигурные скобки, а y — с default-initialization. Для класса с доступным конструктором без аргументов обе записи обычно создают объект, но фигурная форма дополнительно устраняет рассматриваемую синтаксическую неоднозначность.
Нужно различать синтаксическую однозначность и семантику инициализации. Фигурные скобки предотвращают most vexing parse, однако могут изменить выбор конструктора: конструктор с параметром std::initializer_list имеет особые правила при list-initialization, а сужающие преобразования запрещаются.
Другие безопасные формы — явное присваивание временного объекта или вывод типа через auto, если это не ухудшает читаемость. Выбор формы должен учитывать не только устранение неоднозначности, но и требуемый вид инициализации.
В коде инициализации конфигурационного объекта разработчик записал объявление с круглыми скобками, ожидая создать локальный объект. Компилятор принял строку как объявление функции, а последующий вызов метода дал ошибку, не связанную на первый взгляд с исходной строкой.
Рассматривались три варианта. Сохранить круглые скобки было нельзя: запись оставалась декларацией функции. Использовать присваивание временного объекта было однозначно, но добавляло менее современный стиль и могло затруднить анализ инициализации. Фигурные скобки устраняли неоднозначность и явно показывали создание объекта, но требовали проверить отсутствие нежелательного выбора initializer_list.
Выбрали фигурную инициализацию после проверки конструкторов класса. В результате объект создавался в нужном месте, ошибка исчезла, а намерение кода стало очевидным для компилятора и читателя.
Вопрос: Почему const Widget w(); не создаёт константный объект?
Ответ: Потому что это всё ещё объявление функции w, возвращающей const Widget. Ключевое слово const относится к возвращаемому типу, а не к локальному объекту. Кроме того, верхнеуровневый const у возвращаемого значения класса обычно не даёт практической пользы для результата, возвращённого по значению.
Вопрос: Как разбирается запись с типом в скобках параметра, например Widget w(Widget());?
Ответ: Она также может быть разобрана как объявление функции, а не как создание объекта. Внешняя функция w принимает параметр, записанный как функцию без аргументов, возвращающую Widget; в параметрах такой тип корректируется до указателя на функцию. Подобные декларации сложны для чтения, поэтому их следует заменять фигурной инициализацией, auto или явным отдельным описанием типа.
Вопрос: Почему фигурные скобки устраняют syntactic ambiguity, но не всегда являются полной заменой круглым скобкам?
Ответ: Пустые фигурные скобки однозначно обозначают инициализацию объекта, поэтому запись не превращается в объявление функции. Однако list-initialization имеет собственные правила: подходящий конструктор std::initializer_list рассматривается приоритетно, а narrowing conversions запрещаются. Поэтому переход на фигурные скобки может изменить не только разбор, но и вызываемый конструктор; это необходимо проверить при изменении существующего кода.