Правильное распространение переменной 'decltype (auto)' из функции
(Это продолжение "Есть ли реальные варианты использования переменных 'decltype (auto)'?")
Рассмотрим следующий сценарий - я хочу передать функцию f
другой функции invoke_log_return
, которая будет:
Вызвать f
;
Напечатайте что-нибудь на стандартный вывод;
Вернуть результат f
, избегая ненужных копий/перемещений и допуская копирование.
Обратите внимание, что если выбрасывает f
, ничего не должно быть напечатано на стандартный вывод. Это то, что я до сих пор:
template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
decltype(auto) result{std::forward<F>(f)()};
std::printf(" ...logging here...\n");
if constexpr(std::is_reference_v<decltype(result)>)
{
return decltype(result)(result);
}
else
{
return result;
}
}
Давайте рассмотрим различные возможности:
Вы можете увидеть тестовое приложение здесь, на godbolt.org. Как видите, g++
выполняет NRVO для случая prvalue, а clang++
- нет.
Вопросы:
Является ли это кратчайшим способом "идеального" возврата переменной decltype(auto)
из функции? Есть ли более простой способ добиться того, чего я хочу?
Можно ли извлечь шаблон if constexpr { ... } else { ... }
в отдельную функцию? Единственный способ извлечь его - это макрос.
Есть ли веская причина, по которой clang++
не выполняет NRVO для описанного выше случая предварительной оценки? Следует ли сообщать о нем как о потенциальном улучшении, или оптимизация g++
NRVO здесь недопустима?
Вот альтернатива с использованием помощника on_scope_success
(как предложил Барри Ревзин):
template <typename F>
struct on_scope_success : F
{
int _uncaught{std::uncaught_exceptions()};
on_scope_success(F&& f) : F{std::forward<F>(f)} { }
~on_scope_success()
{
if(_uncaught == std::uncaught_exceptions()) {
(*this)();
}
}
};
template <typename F>
decltype(auto) invoke_log_return_scope(F&& f)
{
on_scope_success _{[]{ std::printf(" ...logging here...\n"); }};
return std::forward<F>(f)();
}
Хотя invoke_log_return_scope
намного короче, для этого требуется другая ментальная модель поведения функции и реализация новой абстракции. Удивительно, но и g++
, и clang++
выполняют RVO/copy-elision с этим решением.
живой пример на godbolt.org
Как отметил Бен Фойгт, одним из основных недостатков этого подхода является то, что возвращаемое значение f
не может быть частью сообщения журнала.
Ответы
Ответ 1
Мы можем использовать модифицированную версию std::forward
: (имя не используется для предотвращения проблем с ADL)
template <typename T>
T my_forward(std::remove_reference_t<T>& arg)
{
return std::forward<T>(arg);
}
Этот шаблон функции используется для пересылки переменной decltype(auto)
. Его можно использовать так:
template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
decltype(auto) result{std::forward<F>(f)()};
std::printf(" ...logging here...\n");
return my_forward<decltype(result)>(result);
}
Таким образом, если std::forward<F>(f)()
вернет
значение prvalue, тогда result
не является ссылкой, а invoke_log_return
возвращает тип без ссылки;
lvalue, тогда result
является ссылкой на lvalue, а invoke_log_return
возвращает ссылочный тип lvalue;
xvalue, тогда result
является ссылкой на rvalue, а invoke_log_return
возвращает тип ссылки на rvalue.
(По сути скопировано с моего fooobar.com/questions/18326191/...)
Ответ 2
Это самый простой и понятный способ написать это:
template <typename F>
auto invoke_log_return(F&& f)
{
auto result = f();
std::printf(" ...logging here... %s\n", result.foo());
return result;
}
GCC получает правильный (без лишних копий или перемещений) ожидаемый результат:
s()
in main
prvalue
s()
...logging here... Foo!
lvalue
s(const s&)
...logging here... Foo!
xvalue
s(s&&)
...logging here... Foo!
Таким образом, если код понятен, он имеет те же функциональные возможности, но не оптимизирован для работы столько, сколько конкуренты делают это при сбое оптимизации компилятора, и clang должен решить эту проблему. Такая проблема, которая имеет больше смысла, решается в инструменте, а не на уровне приложения.
https://gcc.godbolt.org/z/50u-hT