Программирование C++C++ CoreВедущий разработчик C++

В чём принципиальная опасность привязки временного объекта к ссылочному члену в списке инициализации констр...

В чём принципиальная опасность привязки временного объекта к ссылочному члену в списке инициализации конструктора?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

В современном стандарте C++ инициализация ссылочного члена временным объектом в списке инициализации конструктора считается некорректной. Исторически такая конструкция обычно принималась компиляторами, но временный объект жил только до завершения конструктора, после чего член становился висячей ссылкой.

Исторический контекст

Ссылочный член хранит только ссылку на существующий объект и не владеет его временем жизни. Для локальной ссылки C++ продлевает время жизни временного объекта до конца области действия ссылки, но это правило не распространяется аналогичным образом на ссылочный член, создаваемый в конструкторе.

Проблема стала настолько существенной, что правило было изменено решением CWG 1696: подобная инициализация должна отвергаться. Однако некоторые компиляторы сохраняют поддержку старого поведения, иногда с предупреждением.

Постановка проблемы

Рассмотрим класс, который хранит ссылку на строку:

#include <string> struct View { const std::string& text; View() : text(std::string("temporary")) {} };

В переносимом современном C++ такой код должен быть диагностирован как некорректный. Если компилятор его принимает, временная строка уничтожается после завершения конструктора View, а text продолжает ссылаться на уже уничтоженный объект.

Использование такой ссылки даёт неопределённое поведение: возможны повреждение данных, ошибочные результаты или падение программы. const делает объект доступным только для чтения, но не продлевает его время жизни и не добавляет владения.

Подробное решение

Временный объект, созданный в списке инициализации конструктора, существует в контексте выполнения конструктора. После завершения конструктора он уничтожается, если другое правило времени жизни не предусматривает продление. Ссылочный член при этом не превращается в копию объекта и не получает самостоятельного хранилища.

Надёжные варианты зависят от семантики класса:

  • хранить объект по значению, если класс должен владеть данными;
  • принимать внешнюю ссылку, но требовать, чтобы внешний объект жил дольше экземпляра класса;
  • хранить std::shared_ptr<const T>, если требуется разделяемое владение;
  • хранить std::string_view только тогда, когда время жизни исходной строки гарантировано отдельно.

std::string_view не исправляет проблему: это невладеющее представление, аналогичное ссылке по смыслу времени жизни. Передача временной строки в объект, который сохраняет string_view, также может привести к висячему представлению.

Ситуация из практики

Компонент форматирования принимает настройки строкой и сохраняет их для последующих вызовов. Разработчик делает ссылочный член, чтобы избежать копирования, и передаёт временный результат сборки строки. На тестах код может работать, потому что память ещё не перезаписана, но после следующей аллокации содержимое становится повреждённым.

Хранение const std::string& экономит копирование, но требует строгого внешнего контракта времени жизни и не допускает временные аргументы. Хранение std::string_view дешевле по размеру, но имеет ту же проблему владения. Хранение std::string создаёт копию или перемещает значение, зато делает объект самостоятельным и безопасным.

Для компонента, который должен принимать как именованные строки, так и временные результаты, выбирают хранение по значению. Дополнительная копия обычно является приемлемой платой за корректное владение; при необходимости перемещение временного std::string уменьшает издержки.

Что кандидаты часто упускают

  1. Продлевает ли const-ссылка в члене время жизни временного объекта?

Нет. const определяет доступ только для чтения и может участвовать в некоторых правилах привязки ссылок, но не превращает ссылочный член во владеющий объект. Время жизни определяется контекстом инициализации, а не наличием const.

  1. Отличается ли инициализация ссылочного элемента агрегата фигурными и круглыми скобками?

Да. Для агрегата, инициализированного фигурными скобками, временный объект, привязанный к ссылочному элементу, обычно получает время жизни агрегата. При инициализации агрегата круглым списком выражений временный объект живёт только до завершения полного выражения, содержащего инициализацию. Это специальное различие C++20, поэтому заменять фигурные скобки круглыми без проверки семантики опасно.

  1. Безопасна ли передача временного объекта в функцию, если параметр имеет тип константной ссылки?

Во время выполнения вызова — да: временный объект, привязанный к ссылочному параметру, живёт до завершения полного выражения, содержащего вызов функции. Но если функция сохраняет адрес или ссылку в поле другого объекта, после завершения вызова сохранённая ссылка может стать висячей.

Следовательно, константная ссылка безопасна для непосредственного использования внутри функции, но не гарантирует безопасность последующего хранения ссылки. Для длительного хранения нужно копировать значение или явно организовать владение объектом.