Совершенная пересылка возвращаемого значения с помощью 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()
.