Composable C++ Function Decorators

南笙酒味 提交于 2019-12-23 14:56:56

问题


Python has a very useful feature of function decorators, which, moreover, allows composition. For example, if write a function foo, then you can state that you would like foo to be memoized, but also retried more than a single time in case of a cache miss in which foo also raises an exception, by:

@lru_cache
@retry
def foo(...):

Decorator composability allows developing functions like foo and individual function decorators independently, and then mixing them as needed. It would be nice if we could do so in C++ as well (to the extent possible).

While there are several questions on StackOverflow regarding function decorators, they all seem to generate non-composable ones, due to rigid assumptions on the signature of the decorated function. E.g., consider the excellent top-voted answer to this question. The decoration is of the form

template <typename R, typename... Args>
std::function<R (Args...)> memo(R (*fn)(Args...)) {

Consequently, it cannot be applied to the result of itself (admittedly not much of an issue for the specific decorator use of memoization).

How can we write composable function decorators, then?


回答1:


Another way to create composable function decorators is by using a set of mixin classes.
It follows a minimal, working example:

#include<iostream>
#include<functional>
#include<utility>
#include<type_traits>

template<class T>
struct LoggerDecoratorA: public T {
    template<class U>
    LoggerDecoratorA(const U &u): T{u} { }

    template<typename... Args>
    auto operator()(Args&&... args) const ->
        typename std::enable_if<
            not std::is_same<
                typename std::result_of<T(Args...)>::type,
                void
            >::value,
        typename std::result_of<T(Args...)>::type>::type
    {
        using namespace std;
        cout << "> logger A" << endl;
        auto ret = T::operator()(std::forward<Args>(args)...);
        cout << "< logger A" << endl;
        return ret;
    }

    template<typename... Args>
    auto operator()(Args&&... args) const ->
        typename std::enable_if<
            std::is_same<
                typename std::result_of<T(Args...)>::type,
                void
            >::value,
        typename std::result_of<T(Args...)>::type>::type
    {
        using namespace std;
        cout << "> logger A" << endl;
        T::operator()(std::forward<Args>(args)...);
        cout << "< logger A" << endl;
    }
};

template<class T>
struct LoggerDecoratorB: public T {
    template<class U>
    LoggerDecoratorB(const U &u): T{u} { }

    template<typename... Args>
    auto operator()(Args&&... args) const ->
        typename std::enable_if<
            not std::is_same<
                typename std::result_of<T(Args...)>::type,
                void
            >::value,
        typename std::result_of<T(Args...)>::type>::type
    {
        using namespace std;
        cout << "> logger B" << endl;
        auto ret = T::operator()(std::forward<Args>(args)...);
        cout << "< logger B" << endl;
        return ret;
    }

    template<typename... Args>
    auto operator()(Args&&... args) const ->
        typename std::enable_if<
            std::is_same<
                typename std::result_of<T(Args...)>::type,
                void
            >::value,
        typename std::result_of<T(Args...)>::type>::type
    {
        using namespace std;
        cout << "> logger B" << endl;
        T::operator()(std::forward<Args>(args)...);
        cout << "< logger B" << endl;
    }
};

int main() {
    std::function<int()> fn = [](){
        using namespace std;
        cout << 42 << endl;
        return 42;
    };

    std::function<void()> vFn = [](){
        using namespace std;
        cout << "void" << endl;
    };

    using namespace std;

    decltype(fn) aFn =
        LoggerDecoratorA<decltype(fn)>(fn);
    aFn();

    cout << "---" << endl;

    decltype(vFn) bVFn =
        LoggerDecoratorB<decltype(vFn)>(vFn);
    bVFn();

    cout << "---" << endl;

    decltype(fn) abFn =
        LoggerDecoratorA<LoggerDecoratorB<decltype(fn)>>(fn);
    abFn();

    cout << "---" << endl;

    decltype(fn) baFn =
        LoggerDecoratorB<LoggerDecoratorA<decltype(fn)>>(fn);
    baFn();
}

I'm not sure what of the problems you mentioned it actually solves, but feel free to ask for changes and I'll try to update it if possible.




回答2:


One way to create composable function decorators, is by relaxing the assumption on the signature taken by the decorator. Say we have

template<class Fn>
struct foo_decorator
{
    template<typename ...Args>
    auto operator()(Args &&...args) const ->
        typename std::result_of<Fn(Args...)>::type;
};

template<class Fn>
foo_decorator<Fn> make_foo(const Fn &fn) {return foo_decorator<Fn>();}

As can be seen, both make_foo and foo_decorator are parameterized by class Fn which can be virtually anything at that point. Hence they can take both a lambda function or a functor, for example. The arguments taken (and the return type) are (compile-time) deferred to the invocation, where the deduced template parameters of a C++ function call will fill in the rest of the details as needed.

Using this, here is a simple logging decorator:

#include <type_traits>
#include <cmath>
#include <iostream>     

template<class Fn> 
struct logger
{   
    logger(const Fn &fn, const std::string &name) : m_fn(fn), m_name{name}{}

    template<typename ...Args>
    auto operator()(Args &&...args) const ->
        typename std::result_of<Fn(Args...)>::type
    {   
        std::cout << "entering " << m_name << std::endl;
        const auto ret = m_fn(std::forward<Args>(args)...);
        std::cout << "leaving " << m_name << std::endl;
        return ret;
    }

private:
    Fn m_fn;
    std::string m_name;
};  

template<class Fn> 
logger<Fn> make_log(const Fn &fn, const std::string &name)
{   
    return logger<Fn>(fn, name);
}   

int main()
{   
    auto fn = make_log([](double x){return std::sin(x);}, "sin");
    std::cout << fn(4.0) << std::endl;
}   

Here is a build of this decorator, here is a build of a retrying decorator, and here is a build of a composition of them.

One drawback of this approach is for cases where the decorator has a state relying on the signature of the function, e.g., the original case of memoization. It's possible to deal with this using type erasure (see a build here), but this has a number of drawbacks, one of which is that errors which conceptually could have been caught at compile time, are now caught at runtime (when the type erasure detects illegal use).



来源:https://stackoverflow.com/questions/36206515/composable-c-function-decorators

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!