Декораторы в C++
В связи с этим вопросом возник такой.
Как правильно написать функцию, которая принимает другую случайную функцию, возможно что-то делает, вызывает принятую функцию, возможно опять что-то делает, а потом возвращает результат этой принятой функции?
При этом все типы должны сохраняться, то есть если принятая функция возвращает что-то по ссылке или по значению, то и результат функции-декоратора должен быть таким же. Параметры тоже должны передаваться идеально. Не должно быть лишних копирований и перемещений (может быть копирование результата исходной функции, только если этот результат нужен нашему декоратору (например, залогировать его), но такое копирование не лишнее, а необходимое). Если переданная функция выкидывает исключение, то исполнение декоратора не происходит (то есть если у нас логирующий декоратор, то лог не пишется). Если для исходной функции была возможна оптимизация возвращающего значения, то и для вызова декорирующей она должна быть.
Пример вызова такой декорирующей функции call_and_log:
struct A {} a;
auto foo = []() -> A { return {}; };
auto bar = []() -> A& { return a; };
int main() {
auto rvo = call_and_log(foo);
auto no_rvo = call_and_log(bar);
}
Ответы (4 шт):
Слева привиндить не проблема:
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';
}
Вы можете использовать такую конструкцию
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 конструктора всегда ложится на самого программиста, потому что старые варианты(без учёта исключений) без оптимизаций самого компилятора тоже будут вызывать копирование по несколько раз. Вообще судя по логам, промежуточного копирования не происходит тык
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;
}
Вот так вот:
#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 before\n";
}
~Logger() {
if(initial_exceptions_number == std::uncaught_exceptions()) {
std::clog << "message after\n";
}
}
} 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 before\n";
}
~Logger() {
if(initial_exceptions_number == std::uncaught_exceptions()) {
std::clog << "message after\n";
}
}
} 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