如何编译装饰器-C ++,Python及其自身的实现。第1部分

本系列文章将致力于使用C ++ 创建装饰器的可能性,其在Python中工作的功能,我们还将考虑使用创建闭包的通用方法-闭包转换和语法树的现代化,以自己的编译语言实现此功能的选项之一。



免责声明
, Python — . Python , (). - ( ), - ( , ..), Python «» .

C ++中的装饰器


这一切始于我的朋友VoidDruid决定编写一个小型编译器作为文凭,其主要功能是装饰器。即使在防御过程中,当他描述自己的方法的所有优点(包括更改AST)时,我也想知道:在功能强大的C ++中实现这些相同的装饰器,而没有任何复杂的术语和方法,真的是不可能的吗?搜寻这个主题,我没有找到任何简单且通用的方法来解决这个问题(顺便说一句,我只看了有关设计模式实现的文章),然后坐下来编写自己的装饰器。


但是,在继续直接介绍我的实现之前,我想先谈谈C ++中的lambda和闭包是如何排列的以及它们之间的区别。立即保留一下,如果没有提及特定标准,那么默认情况下我的意思是C ++ 20。简而言之,lambda是匿名函数,而闭包是使用环境中对象的函数。因此,例如,从C ++ 11开始,可以这样声明和调用lambda:

int main() 
{
    [] (int a) 
    {
        std::cout << a << std::endl;
    }(10);
}

或者将其值分配给变量,然后再调用它。

int main() 
{
    auto lambda = [] (int a) 
    {
        std::cout << a << std::endl;
    };
    lambda(10);
}

但是编译期间会发生什么,什么是lambda?要使自己沉浸在lambda的内部结构中,只需转到cppinsights.io并运行我们的第一个示例。接下来,我附上一个可能的结论:

class __lambda_60_19
{
public: 
    inline void operator()(int a) const
    {
        std::cout.operator<<(a).operator<<(std::endl);
    }
    
    using retType_60_19 = void (*)(int);
    inline operator retType_60_19 () const noexcept
    {
        return __invoke;
    };
    
private: 
    static inline void __invoke(int a)
    {
        std::cout.operator<<(a).operator<<(std::endl);
    }    
};


因此,在编译时,lambda会变成一个类,或者是一个函子(一个为其定义了operator()的对象),它具有一个自动生成的唯一名称,该名称具有一个operator(),它接受我们传递给lambda且其主体包含的参数lambda必须执行的代码。这样,一切都很清楚,但是其他两种方法又为什么呢?第一个是强制转换为函数指针的运算符,其原型与我们的lambda一致,第二个是在初步分配给它的指针时调用我们的lambda时应执行的代码,例如:

void (*p_lambda) (int) = lambda;
p_lambda(10);

好吧,一个谜就少了,但是闭包呢?让我们写一个最简单的闭包示例,该闭包通过引用捕获变量“ a”并将其增加一个。

int main()
{
    int a = 10;
    auto closure = [&a] () { a += 1; };
    closure();
}

如您所见,在C ++中创建闭包和lambda的机制几乎相同,因此这些概念经常被混淆,lambda和闭包简称为lambda。

但是回到C ++中闭包的内部表示。

class __lambda_61_20
{
public:
    inline void operator()()
    {
        a += 1;
    }
private:
    int & a;
public:
    __lambda_61_20(int & _a)
    : a{_a}
    {}
};

如您所见,我们添加了一个新的非默认构造函数,该构造函数通过引用获取我们的参数并将其保存为该类的成员。实际上,这就是为什么在设置[&]或[=]时需要格外小心的原因,因为整个上下文都将存储在闭包中,而这在内存中可能不是最佳选择。另外,我们丢失了转换为函数指针的运算符,因为现在需要其正常的调用上下文。现在上面的代码将无法编译:

int main()
{
    int a = 10;
    auto closure = [&a] () { a += 1; };
    closure();
    void (*ptr)(int) = closure;
}

但是,如果仍然需要将闭包传递给某个地方,则没有人取消使用std ::函数。

std::function<void()> function = closure;
function();

现在,我们已经大致弄清楚了C ++中的lambda和闭包,让我们继续直接编写装饰器。但是首先,您需要决定我们的要求。

因此,装饰器应将我们的函数或方法作为输入,向其添加所需的功能(例如,将省略此功能),并在调用时返回一个新函数,该新函数将执行我们的代码和函数/方法代码。在这一点上,任何自尊的python专家都会说:“但是!装饰器必须替换原始对象,并且通过名称对其进行的任何调用都应调用一个新函数!”仅仅是这是C ++的主要限制,我们不能阻止用户调用旧函数。当然,可以选择在内存中获取其地址并对其进行研磨(在这种情况下,访问它会导致程序异常终止),或者用警告其不应在控制台中使用的警告替换其主体,但这会带来严重的后果。如果第一种选择似乎很难,那么第二种方法在使用各种编译器优化时也会导致崩溃,因此我们将不使用它们。此外,在这里我认为使用任何宏魔术都是多余的。

因此,让我们继续编写装饰器。我想到的第一个选择是:

namespace Decorator
{
    template<typename R, typename ...Args>
    static auto make(const std::function<R(Args...)>& f)
    {
        std::cout << "Do something" << std::endl;
        return [=](Args... args) 
        {
            return f(std::forward<Args>(args)...);
        };
    }
};

假定它是一个带有静态方法的结构,该方法采用std ::函数并返回一个闭包,闭包将采用与我们的函数相同的参数,并且在调用时,它将仅调用我们的函数并返回其结果。

让我们创建一个我们想要装饰的简单函数。

void myFunc(int a)
{
    std::cout << "here" << std::endl;
}

我们的主体看起来像这样:

int main()
{
    std::function<void(int)> f = myFunc;
    auto decorated = Decorator::make(f);
    decorated(10);
}


一切正常,一切都很好,总的来说,华友世纪。

实际上,该解决方案有几个问题。让我们按顺序开始:

  1. 此代码只能使用C ++ 14及更高版本进行编译,因为无法提前知道返回的类型。不幸的是,我不得不忍受这一点,而我没有找到其他选择。
  2. make需要将std ::函数传递给它,并且按名称传递函数会导致编译错误。这根本没有我们想要的方便!我们不能这样写代码:

    Decorator::make([](){});
    Decorator::make(myFunc);
    void(*ptr)(int) = myFunc;
    Decorator::make(ptr);

  3. 同样,无法装饰类方法。

因此,在与同事简短交谈之后,为C ++ 17及更高版本发明了以下选项:

namespace Decorator
{
    template<typename Function>
    static auto make(Function&& func)
    {
        return [func = std::forward<Function>(func)] (auto && ...args) 
        {
            std::cout << "Do something" << std::endl;
            return std::invoke(
                func,
                std::forward<decltype(args)>(args)...
            );
        };
    }
};

此特定选项的优点在于,现在我们可以绝对装饰任何具有operator()的对象因此,例如,我们可以传递自由函数的名称,指针,lambda,任何函子,std ::函数,当然还有类方法。对于后者,在调用解码函数时也有必要将上下文传递给它。

应用选项
int main()
{
    auto decorated_1 = Decorator::make(myFunc);
    decorated_1(1,2);

    auto my_lambda = [] (int a, int b) 
    { 
        std::cout << a << " " << b <<std::endl; 
    };
    auto decorated_2 = Decorator::make(my_lambda);
    decorated_2(3,4);

    int (*ptr)(int, int) = myFunc;
    auto decorated_3 = Decorator::make(ptr);
    decorated_3(5,6);

    std::function<void(int, int)> fun = myFunc;
    auto decorated_4 = Decorator::make(fun);
    decorated_4(7,8);

    auto decorated_5 = Decorator::make(decorated_4);
    decorated_5(9, 10);

    auto decorated_6 = Decorator::make(&MyClass::func);
    decorated_6(MyClass(10));
}


此外,如果存在使用std :: invoke的扩展,则可以用C ++ 14编译该代码,而该扩展需要用std :: ____ invoke替换。如果没有扩展名,那么您将不得不放弃装饰类方法的能力,并且该功能将变得不可用。

为了不编写繁琐的“ std :: forward <decltype(args)>(args)...”,您可以使用C ++ 20提供的功能,并制作我们的lambda样板!

namespace Decorator
{
    template<typename Function>
    static auto make(Function&& func)
    {
        return [func = std::forward<Function>(func)] 
        <typename ...Args> (Args && ...args) 
        {
            return std::invoke(
                func,
                std::forward<Args>(args)...
            );
        };
    }
};

一切都非常安全,甚至可以按照我们想要的方式工作(或至少假装)。该代码针对gcc和clang 10-x版本进行了编译,您可以在此处找到也将有各种标准的实现。

在下一篇文章中,我们将使用Python示例及其内部结构继续装饰器的规范实现。

All Articles