Сравните две функции и определите их точные типы возвращаемого значения. Объясните, почему скобки меняют результат вывода через decltype(auto).
int value = 10;
decltype(auto) by_name() {
return value;
}
decltype(auto) by_expression() {
return (value);
}
by_name() возвращает int по значению, а by_expression() — int&. Причина в специальном правиле decltype: для не заключённого в скобки идентификатора он возвращает объявленный тип сущности, а для выражения (value) применяет правила категорий значений и определяет тип как ссылку на lvalue.
Поэтому результат by_name() нельзя использовать для изменения глобальной переменной, а результат by_expression() можно: он является ссылкой на value.
decltype появился в C++11 как средство получить точный тип выражения во время компиляции, включая ссылочность и категорию значения. Это было важно для шаблонного кода, где тип результата нельзя надёжно записать вручную.
В C++14 появился decltype(auto) — форма автоматического вывода типа, которая использует именно правила decltype, а не обычные правила вывода auto. Это позволяет шаблонным функциям сохранять ссылку или возвращать значение намеренно.
Небольшое изменение синтаксиса в возвращаемом выражении может изменить контракт функции: вместо копии она начнёт возвращать ссылку на объект. Неверный выбор приводит либо к лишнему копированию, либо к неожиданному изменению исходного объекта через результат функции.
Особенно опасно возвращать ссылку на локальный объект или на подобъект временного объекта. В таком случае после завершения функции ссылка может стать висячей, а использование результата — неопределённым поведением.
Для decltype(value) действует специальное исключение: если value — не заключённый в скобки идентификатор, decltype возвращает объявленный тип переменной, то есть int.
Выражение (value) уже не считается таким идентификатором. Переменная value является lvalue, поэтому decltype((value)) равен int&. Следовательно, decltype(auto) в by_expression() выводит int&.
Обычный auto ведёт себя иначе:
auto при выводе отбрасывает верхнеуровневые ссылки и квалификаторы, если они не переданы явно. decltype(auto) сохраняет точный результат применения decltype к возвращаемому выражению.
У decltype(auto) все возвращаемые выражения в функции должны приводить к совместимому выводимому типу. Нельзя в одной функции в одной ветви вернуть int, а в другой — int&.
Возврат ссылки оправдан, когда функция должна предоставить доступ к существующему объекту, например к полю объекта, переданного по ссылке. Возврат значения безопаснее, если объект может быть временным, владение данными неочевидно или копирование недорого.
В библиотечном адаптере нужно вернуть поле объекта без лишнего копирования:
Вариант с auto вернул бы std::string по значению и не позволил бы изменить исходное поле через результат. Вариант decltype(auto) без скобок для return record.name применил бы специальное правило для идентификатора-члена и вернул значение, что также не соответствует цели.
Выбранный вариант с return (record.name) возвращает std::string&: копирования нет, а присваивание изменяет record.name. Компромисс — вызывающий код получает ссылку, срок жизни которой ограничен сроком жизни объекта record; поэтому параметр сделан ссылкой на lvalue и функция не принимает временные объекты.
Что произойдёт, если заменить decltype(auto) на auto только в by_expression()?
Функция начнёт возвращать int по значению. Скобки сохранятся в исходном выражении, но правила вывода auto не сохраняют ссылочность результата. Изменение возвращённой копии не повлияет на value.
Почему return member; и return (member); могут давать разные типы?
Для decltype не заключённый в скобки идентификатор, включая имя нестатического члена в соответствующем выражении, обрабатывается по специальному правилу и даёт объявленный тип. После добавления скобок выражение рассматривается по общей схеме: если это lvalue типа T, результатом становится T&.
Безопасно ли возвращать ссылку на поле объекта, принятого по ссылке?
Это безопасно только пока живёт сам объект-владелец поля. Ссылка не продлевает время жизни объекта и не становится независимой копией. Если функция принимает только T&, временный объект передать нельзя; если принимать T&& или универсальную ссылку без дополнительных ограничений, можно случайно вернуть ссылку на поле уже уничтоженного временного объекта.