Stack Overflow на русском Asked on November 20, 2021
В связи с этим вопросом возник такой.
Как правильно написать функцию, которая принимает другую случайную функцию, возможно что-то делает, вызывает принятую функцию, возможно опять что-то делает, а потом возвращает результат этой принятой функции?
При этом все типы должны сохраняться, то есть если принятая функция возвращает что-то по ссылке или по значению, то и результат функции-декоратора должен быть таким же. Параметры тоже должны передаваться идеально. Не должно быть лишних копирований и перемещений (может быть копирование результата исходной функции, только если этот результат нужен нашему декоратору (например, залогировать его), но такое копирование не лишнее, а необходимое). Если переданная функция выкидывает исключение, то "опять что-то делает" не происходит (то есть если у нас должен писаться лог после вызова исходной функции, то при исключении он не пишется). Если для исходной функции была возможна оптимизация возвращающего значения, то и для вызова декорирующей она должна быть.
Пример вызова такой декорирующей функции log_invoke
:
struct A {} a;
auto foo = []() -> A { return {}; };
auto bar = []() -> A& { return a; };
int main() {
auto rvo = log_invoke(foo);
auto no_rvo = log_invoke(bar);
}
Вот так вот:
#include <concepts>
#include <exception>
#include <iostream>
#include <functional>
#include <utility>
template<typename F, typename... Args>
requires std::invocable<F, Args...>
decltype(auto)
log_invoke(F&& f, Args&&... args) {
struct Logger {
const int initial_exceptions_number{std::uncaught_exceptions()};
Logger() {
std::clog << "message beforen";
}
~Logger() {
if(initial_exceptions_number == std::uncaught_exceptions()) {
std::clog << "message aftern";
}
}
} logger;
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}
Плюсы:
Гарантированная оптимизация возвращаемого значения (gcc и clang)
Работает для функций, возвращающих void
!
Немного более понятная диагностика компилятора при вызове с неправильными аргументами из-за std::invocable<F, Args...>
(но в другие решения концепт тоже элементарно добавить, так что это микроплюс)
Минусы:
Читаемость? Но если делать обобщённую версию (в конструкторе принимать 2 функции), то объявление класса можно вынести за пределы функций-декораторов и в итоге функции будут состоять из двух строк: создания экземпляра класса с передачей 2 функций в конструктор и return
стейтмента, что в итоге будет меньше, чем писать constexpr if
(+1 ветка для возвращаемого типа void
помимо уже существующих 2)
Дополнительный int
по памяти, 2 вызова функции и сравнение интов по времени
Нельзя использовать возвращаемое значение
Тест:
#include <concepts>
#include <exception>
#include <iostream>
#include <functional>
#include <utility>
template<typename F, typename... Args>
requires std::invocable<F, Args...>
decltype(auto)
log_invoke(F&& f, Args&&... args) {
struct Logger {
const int initial_exceptions_number{std::uncaught_exceptions()};
Logger() {
std::clog << "message beforen";
}
~Logger() {
if(initial_exceptions_number == std::uncaught_exceptions()) {
std::clog << "message aftern";
}
}
} logger;
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}
struct A {
A() { std::clog << "A()n"; }
A(const A&) { std::clog << "A(const A&)n"; }
A(A&&) noexcept { std::clog << "A(A&&)n"; }
};
A foo() { return A{}; }
int main() {
const auto rvo = foo();
const auto rvo_too = log_invoke(foo);
std::clog << 'n';
void (*void_foo_ptr)(int, int) = [](int x, int y) { std::clog << x << ' ' << y << 'n'; };
log_invoke(void_foo_ptr, 1, 2);
// const auto not_invocable = "I'm not invocable D:";
// log_invoke(not_invocable);
}
Вывод:
A()
message before
A()
message after
message before
1 2
message after
Answered by dIm0n on November 20, 2021
template <typename TFunc>
auto DoSomething(TFunc &&func) -> std::invoke_result_t<TFunc>
{
std::cout << "DoSomething" << std::endl;
decltype(auto) res = func();
std::cout << "DoSomething2" << std::endl;
return res;
}
Answered by Антон on November 20, 2021
Вы можете использовать такую конструкцию
struct A {} a;
auto foo = []() -> A {return {};};
auto bar = []() -> A& {return a;};
template<class Function>
decltype(auto) decorator(Function&& function){
std::cout << "Decorator called" << std::endl;
return std::forward<decltype(function())>(function());
}
int main()
{
decltype(auto) rvo = decorator(foo);
decltype(auto) no_rvo = decorator(bar);
return 0;
}
Если с аргументами, то такую
template<class Function, class... Args>
decltype(auto) decorator(Function&& function, Args&&... args){
std::cout << "Decorator called" << std::endl;
return std::forward<decltype(function(std::forward<Args>(args)...))>(function(std::forward<Args>(args)...));
}
Если с исключениями
template<class Function, class... Args>
decltype(auto) decorator(Function&& function, Args&&... args){
decltype(auto) result = function(std::forward<Args>(args)...);
std::cout << "Decorator called" << std::endl;
return std::forward<decltype(result)>(result);
}
Единственный минус в том, что если результат не ссылка и не определено перемещение, то будет копирование
В конце концов немного поигравшись я пришёл к выводу, что можно использовать такую конструкцию
template<class Function, class... Args>
decltype(auto) decorator(Function&& function, Args&&... args){
auto&& result = function(std::forward<Args>(args)...);
std::cout << "Decorator called" << std::endl;
return std::forward<decltype(result)>(result);
}
Всё же реализация move конструктора всегда ложится на самого программиста, потому что старые варианты(без учёта исключений) без оптимизаций самого компилятора тоже будут вызывать копирование по несколько раз. Вообще судя по логам, промежуточного копирования не происходит тык
Answered by Кирилл Чуркин on November 20, 2021
Слева привиндить не проблема:
template<class Pre, class F> auto wrap(Pre &&pre, F &&f) {
return [pre = std::move(pre), f = std::move(f)](auto &&...args) -> decltype(auto) {
pre(args...);
return std::invoke(f, std::forward<decltype(args)>(args)...);
};
}
Добавить еще Post несколько сложнее, придется, вероятно, отказаться от std::forward в вызове f.
(forward<decltype(args)>
— это по C++17-ному, в двадцатых можно в template parameters у лямбд.)
Если просто вызвать немного функций с одними и теми же аргументами:
template<class F, class G, class... Args> decltype(auto) sequence(
F &&, G &&g, Args &&...args)
{
std::invoke(std::forward<F>(f), args...);
return std::invoke(std::forward<G>(g), std::forward<Args>(args)...);
}
int &f(int &x) { return x += 2; }
int main() {
int x = 38;
sequence([](int x) { std::cout << "Initial x: " << x << 'n'; }
, f
, x) += 2;
std::cout << x << 'n';
}
Можете привести что-то типа call_and_log, которая вернёт результат переданной функции и залогирует то, что был совершён вызов?
Ох.
template<class F, class... Args> decltype(auto) log_invoke(F &&f, Args &&...args) {
std::clog << "f(";
std::size_t i{};
((std::clog << (i++? ", " : "") << args), ...) << ") = " << std::flush;
using RV = std::invoke_result_t<F, Args...>;
RV rv = std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
std::clog << rv << 'n';
if constexpr(std::is_reference_v<RV>) return std::forward<RV>(rv);
else return rv;
}
int &f(int &x, int z) { return x += z; }
int main() {
int x = 36;
std::cout << x << 'n';
log_invoke(f, x, 4) += 2;
std::cout << x << 'n';
}
Answered by bipll on November 20, 2021
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP