Совершенная пересылка возвращаемого значения с помощью auto &&

Рассмотрим эту цитату из С++ Templates: The Complete Guide (2nd Edition):

decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                               std::forward<Args>(args)...)};
...
return ret;

Обратите внимание, что объявление ret с помощью auto&& неверно. Как reference, auto&& продлевает срок службы возвращаемого значения до тех пор, пока конец его области , но не выше инструкции return для вызывающей функции.

Автор говорит, что auto&& не подходит для идеальной пересылки возвращаемого значения. Однако, не decltype(auto) также формирует ссылку на значение xvalue/lvalue? IMO, decltype(auto), то страдает от одной и той же проблемы. Тогда, какова точка автора?

EDIT:

Вышеприведенный фрагмент кода должен войти внутрь этого шаблона функции.

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
    // here
}

Ответ 1

Здесь два вывода. Один из возвращаемого выражения и один из выражения std::invoke. Поскольку decltype(auto) выводится как объявленный тип для unparenthesized id-expression, мы можем сосредоточиться на выводе из выражения std::invoke.

Цитата из [dcl.type.auto.deduct] пункт 5:

Если заполнителем является спецификатор типа decltype(auto), T должен быть только заполнителем. Тип, выводимый для T, определяется, как описано в [dcl.type.simple], как будто e был операндом decltype.

И цитируется из [dcl.type.simple] пункт 4:

Для выражения e тип, обозначенный decltype(e), определяется следующим образом:

  • if e - это несферированное id-выражение, называя структурированное связывание ([dcl.struct.bind]), decltype(e) является ссылочным типом, указанным в спецификации декларации структурированного связывания;

  • в противном случае, если e - это несвязанное id-выражение или unparenthesized доступ к члену класса, decltype(e) - это тип объекта с именем e. Если такой объект отсутствует или если e называет набор перегруженных функций, программа плохо сформирована;

  • в противном случае, если e - значение x, decltype(e) - T&&, где T - тип e;

  • в противном случае, если e является lvalue, decltype(e) является T&, где T является типом e;

  • в противном случае decltype(e) является типом e.

Примечание decltype(e) выводится T вместо T&&, если e является значением prvalue. В этом отличие от auto&&.

Итак, если std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...) является prvalue, например, возвращаемый тип Callable не является ссылкой, т.е. возвращает по значению, ret выводится как один и тот же тип вместо ссылки, которая совершенно вперед семантику возврата по значению.

Ответ 2

Однако, не decltype (auto) также формирует ссылку на значение xvalue/lvalue?

Нет.

Часть магии decltype(auto) заключается в том, что она знает, что ret является lvalue, поэтому она не будет содержать ссылку.

Если вы написали return (ret), он действительно разрешил ссылочный тип, и вы вернете ссылку на локальную переменную.

tl; dr: decltype(auto) не всегда совпадает с auto&&.

Ответ 3

auto&& всегда является ссылочным типом. С другой стороны, decltype(auto) может быть либо ссылкой, либо типом значения, в зависимости от используемого инициализатора.

Поскольку ret в операторе return не заключен в круглые скобки, выводимый тип call() зависит только от объявленного типа объекта ret, а не от категории значения выражения ret:

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
   decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                                  std::forward<Args>(args)...)};
   ...
   return ret;
}

Если Callable возвращает значение, то категория значения выражения вызова op будет prvalue. В таком случае:

  • decltype(auto) выведет res как decltype(auto) тип (т.е. тип значения).
  • auto&& выведет res как ссылочный тип.

Как объяснено выше, decltype(auto) в call() просто приводит к тому же типу, что и res. Следовательно, если бы auto&& использовалось бы для определения типа res вместо decltype(auto), возвращаемый тип call() был бы ссылкой на локальный объект ret, который не существует после возврата call().