可组合C++功能装饰器

Composable C++ Function Decorators

本文关键字:功能 C++ 可组合      更新时间:2023-10-16

Python具有函数装饰器的一个非常有用的特性,此外,它允许组合。例如,如果编写一个函数foo,那么您可以声明您希望记住foo,但在缓存未命中的情况下也可以重试多次,其中foo也会引发异常,方法是:

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

装饰器可组合性允许独立开发foo和单个功能装饰器等函数,然后根据需要混合它们。如果我们也能在C++这样做(尽可能(,那就太好了。

虽然 StackOverflow 上有几个关于函数装饰器的问题,但由于对装饰函数签名的严格假设,它们似乎都生成了不可组合的问题。 例如,考虑这个问题的优秀最高投票答案。装饰是形式

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

因此,它不能应用于其本身的结果(诚然,对于记忆的特定装饰者使用来说,这不是什么大问题(。

那么,我们如何编写可组合的函数装饰器呢?

创建可组合函数装饰器的另一种方法是使用一组 mixin 类。
它遵循一个最小的工作示例:

#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();
}

我不确定您提到的它实际上解决了什么问题,但请随时要求更改,如果可能的话,我会尝试更新它。

创建可组合函数装饰器的一种方法是放宽对装饰器所取签名的假设。假设我们有

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>();}

可以看出,make_foofoo_decorator都是按class Fn参数化的,在这一点上几乎可以是任何东西。因此,例如,它们可以同时采用 lambda 函数或函子。采用的参数(和返回类型(被(编译时(推迟到调用,其中C++函数调用的推导模板参数将根据需要填充其余详细信息。

使用它,这里有一个简单的日志记录装饰器:

#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;
}   
这是这个装饰器的构建,

这是一个重试装饰器的构建,这是它们的组合的构建。

这种方法的一个缺点是装饰器具有依赖于函数签名的状态的情况,例如,原始的记忆情况。可以使用类型擦除来处理这个问题(请参阅此处的构建(,但这有许多缺点,其中之一是概念上可能在编译时捕获的错误现在在运行时捕获(当类型擦除检测到非法使用时(。