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

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

int value = 10;

decltype(auto) by_name() {
    return value;
}

decltype(auto) by_expression() {
    return (value);
}
Проходите собеседования с ИИ помощником Hintsage

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

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 ведёт себя иначе:

int value = 10; auto copy() { return (value); // int } decltype(auto) reference() { return (value); // int& }

auto при выводе отбрасывает верхнеуровневые ссылки и квалификаторы, если они не переданы явно. decltype(auto) сохраняет точный результат применения decltype к возвращаемому выражению.

У decltype(auto) все возвращаемые выражения в функции должны приводить к совместимому выводимому типу. Нельзя в одной функции в одной ветви вернуть int, а в другой — int&.

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

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

В библиотечном адаптере нужно вернуть поле объекта без лишнего копирования:

struct Record { std::string name; }; template<class T> decltype(auto) name_of(T& record) { return (record.name); } int main() { Record record{"Ada"}; name_of(record) = "Grace"; }

Вариант с auto вернул бы std::string по значению и не позволил бы изменить исходное поле через результат. Вариант decltype(auto) без скобок для return record.name применил бы специальное правило для идентификатора-члена и вернул значение, что также не соответствует цели.

Выбранный вариант с return (record.name) возвращает std::string&: копирования нет, а присваивание изменяет record.name. Компромисс — вызывающий код получает ссылку, срок жизни которой ограничен сроком жизни объекта record; поэтому параметр сделан ссылкой на lvalue и функция не принимает временные объекты.

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

  1. Что произойдёт, если заменить decltype(auto) на auto только в by_expression()?

    Функция начнёт возвращать int по значению. Скобки сохранятся в исходном выражении, но правила вывода auto не сохраняют ссылочность результата. Изменение возвращённой копии не повлияет на value.

  2. Почему return member; и return (member); могут давать разные типы?

    Для decltype не заключённый в скобки идентификатор, включая имя нестатического члена в соответствующем выражении, обрабатывается по специальному правилу и даёт объявленный тип. После добавления скобок выражение рассматривается по общей схеме: если это lvalue типа T, результатом становится T&.

  3. Безопасно ли возвращать ссылку на поле объекта, принятого по ссылке?

    Это безопасно только пока живёт сам объект-владелец поля. Ссылка не продлевает время жизни объекта и не становится независимой копией. Если функция принимает только T&, временный объект передать нельзя; если принимать T&& или универсальную ссылку без дополнительных ограничений, можно случайно вернуть ссылку на поле уже уничтоженного временного объекта.