Декораторы в 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 шт):

Автор решения: bipll

Слева привиндить не проблема:

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;
}
→ Ссылка
Автор решения: dIm0n

Вот так вот:

#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
→ Ссылка