So kompilieren Sie einen Dekorator - C ++, Python und seine eigene Implementierung. Teil 1

Diese Artikelserie befasst sich mit der Möglichkeit, einen Dekorator in C ++ zu erstellen , den Funktionen ihrer Arbeit in Python, und wir werden auch eine der Optionen für die Implementierung dieser Funktionalität in unserer eigenen kompilierten Sprache betrachten, wobei der allgemeine Ansatz zum Erstellen von Closures verwendet wird - Closure-Konvertierung und Modernisierung des Syntaxbaums.



Haftungsausschluss
, Python — . Python , (). - ( ), - ( , ..), Python «» .

Dekorateur in C ++


Alles begann mit der Tatsache, dass mein Freund VoidDruid beschlossen hat, einen kleinen Compiler als Diplom zu schreiben, dessen Hauptmerkmal Dekorateure sind. Selbst während der Vorverteidigung, als er alle Vorteile seines Ansatzes skizzierte, einschließlich der Änderung des AST, fragte ich mich: Ist es wirklich unmöglich, dieselben Dekorateure in das großartige und leistungsstarke C ++ zu implementieren und auf komplizierte Begriffe und Ansätze zu verzichten? Als ich dieses Thema googelte, fand ich keine einfachen und allgemeinen Ansätze zur Lösung dieses Problems (übrigens stieß ich nur auf Artikel über die Implementierung des Entwurfsmusters) und setzte mich dann hin, um meinen eigenen Dekorateur zu schreiben.


Bevor ich jedoch zu einer direkten Beschreibung meiner Implementierung übergehe, möchte ich kurz darauf eingehen, wie Lambdas und Closures in C ++ angeordnet sind und was der Unterschied zwischen ihnen ist. Machen Sie sofort einen Vorbehalt, dass ich standardmäßig C ++ 20 meine, wenn kein bestimmter Standard erwähnt wird. Kurz gesagt, Lambdas sind anonyme Funktionen, und Schließungen sind Funktionen, die Objekte aus ihrer Umgebung verwenden. So kann beispielsweise ab C ++ 11 ein Lambda wie folgt deklariert und aufgerufen werden:

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

Oder weisen Sie den Wert einer Variablen zu und rufen Sie ihn später auf.

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

Aber was passiert beim Kompilieren und was ist Lambda? Um in die interne Struktur des Lambda einzutauchen, besuchen Sie einfach die Website cppinsights.io und führen Sie unser erstes Beispiel aus. Als nächstes habe ich eine mögliche Schlussfolgerung angehängt:

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


Beim Kompilieren verwandelt sich das Lambda in eine Klasse oder vielmehr in einen Funktor (ein Objekt, für das der Operator () definiert ist ) mit einem automatisch generierten eindeutigen Namen, der einen Operator () enthält , der die Parameter übernimmt, die wir an unser Lambda übergeben haben und deren Körper enthält der Code, den unser Lambda ausführen muss. Damit ist alles klar, aber was ist mit den beiden anderen Methoden, warum sind sie? Der erste ist der Operator für das Umwandeln in einen Funktionszeiger, dessen Prototyp mit unserem Lambda übereinstimmt, und der zweite ist der Code, der ausgeführt werden soll, wenn unser Lambda nach vorläufiger Zuweisung zu seinem Zeiger aufgerufen wird, beispielsweise wie folgt:

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

Nun, es gibt weniger Rätsel, aber was ist mit Schließungen? Schreiben wir das einfachste Beispiel für einen Abschluss, der die Variable „a“ als Referenz erfasst und um eins erhöht.

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

Wie Sie sehen können, ist der Mechanismus zum Erstellen von Closures und Lambdas in C ++ nahezu identisch. Daher werden diese Konzepte häufig verwechselt und Lambdas und Closures werden einfach als Lambdas bezeichnet.

Aber zurück zur internen Darstellung des Abschlusses in C ++.

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

Wie Sie sehen können, haben wir einen neuen, nicht standardmäßigen Konstruktor hinzugefügt, der unseren Parameter als Referenz verwendet und als Mitglied der Klasse speichert. Aus diesem Grund müssen Sie beim Festlegen von [&] oder [=] äußerst vorsichtig sein, da der gesamte Kontext innerhalb des Abschlusses gespeichert wird und dies aus dem Speicher heraus möglicherweise nicht optimal ist. Außerdem haben wir den Operator des Castings auf einen Funktionszeiger verloren, da jetzt für seinen normalen Aufruf Kontext benötigt wird. Und jetzt wird der obige Code nicht kompiliert:

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

Wenn Sie den Abschluss jedoch noch irgendwo übergeben müssen, hat niemand die Verwendung der Funktion std :: abgebrochen.

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

Nachdem wir grob herausgefunden haben, was Lambdas und Verschlüsse in C ++ sind, schreiben wir den Dekorateur direkt. Aber zuerst müssen Sie sich für unsere Anforderungen entscheiden.

Der Dekorateur sollte also unsere Funktion oder Methode als Eingabe verwenden, die benötigte Funktionalität hinzufügen (dies wird beispielsweise weggelassen) und beim Aufruf eine neue Funktion zurückgeben, die unseren Code und den Funktions- / Methodencode ausführt. An diesem Punkt wird jeder selbstbewusste Pythonist sagen: „Aber wie so! Der Dekorateur muss das ursprüngliche Objekt ersetzen und jeder Aufruf mit Namen sollte eine neue Funktion aufrufen! “ Nur dies ist die Hauptbeschränkung von C ++. Wir können den Benutzer nicht davon abhalten, die alte Funktion aufzurufen. Natürlich gibt es die Möglichkeit, die Adresse im Speicher abzurufen und zu mahlen (in diesem Fall führt der Zugriff darauf zu einer abnormalen Beendigung des Programms) oder den Textkörper durch eine Warnung zu ersetzen, dass er nicht in der Konsole verwendet werden sollte. Dies ist jedoch mit schwerwiegenden Konsequenzen verbunden. Wenn die erste Option überhaupt ziemlich schwierig erscheint,dann kann die zweite, wenn verschiedene Compiler-Optimierungen verwendet werden, ebenfalls zu einem Absturz führen, weshalb wir sie nicht verwenden werden. Auch die Verwendung von Makromagie halte ich hier für überflüssig.

Schreiben wir also unseren Dekorateur. Die erste Option, die mir in den Sinn kam, war folgende:

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

Es sei eine Struktur mit einer statischen Methode, die std :: function verwendet und einen Abschluss zurückgibt, der dieselben Parameter wie unsere Funktion annimmt. Wenn sie aufgerufen wird, ruft sie einfach unsere Funktion auf und gibt ihr Ergebnis zurück.

Lassen Sie uns eine einfache Funktion erstellen, die wir dekorieren möchten.

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

Und unser Haupt wird so aussehen:

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


Alles funktioniert, alles ist in Ordnung und im Allgemeinen Hurra.

Tatsächlich weist diese Lösung mehrere Probleme auf. Beginnen wir in der richtigen Reihenfolge:

  1. Dieser Code kann nur mit Version C ++ 14 und höher kompiliert werden, da der zurückgegebene Typ nicht im Voraus bekannt ist. Leider muss ich damit leben und habe keine anderen Möglichkeiten gefunden.
  2. Für make muss die Funktion std :: übergeben werden, und die Übergabe einer Funktion nach Namen führt zu Kompilierungsfehlern. Und das ist überhaupt nicht so bequem, wie wir möchten! Wir können keinen Code wie diesen schreiben:

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

  3. Es ist auch nicht möglich, eine Klassenmethode zu dekorieren.

Daher wurde nach einem kurzen Gespräch mit Kollegen die folgende Option für C ++ 17 und höher erfunden:

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

Die Vorteile dieser speziellen Option sind, dass wir jetzt absolut jedes Objekt mit einem Operator () dekorieren können . So können wir beispielsweise den Namen einer freien Funktion, eines Zeigers, eines Lambda, eines beliebigen Funktors, einer std :: -Funktion und natürlich einer Klassenmethode übergeben. Im letzteren Fall muss beim Aufrufen der dekodierten Funktion auch ein Kontext übergeben werden.

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


Darüber hinaus kann dieser Code mit C ++ 14 kompiliert werden, wenn es eine Erweiterung für die Verwendung von std :: invoke gibt, die durch std :: __ invoke ersetzt werden muss. Wenn es keine Erweiterung gibt, müssen Sie die Möglichkeit zum Dekorieren von Klassenmethoden aufgeben, und diese Funktionalität ist nicht mehr verfügbar.

Um das umständliche "std :: forward <decltype (args)> (args) ..." nicht zu schreiben, können Sie die mit C ++ 20 verfügbare Funktionalität nutzen und unser Lambda-Boilerplate erstellen!

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

Alles ist absolut sicher und funktioniert sogar so, wie wir es wollen (oder zumindest so tun). Dieser Code wurde sowohl für gcc- als auch für clang 10-x-Versionen kompiliert und ist hier zu finden . Es wird auch Implementierungen für verschiedene Standards geben.

In den nächsten Artikeln werden wir uns mit der kanonischen Implementierung von Dekoratoren am Python-Beispiel und ihrer internen Struktur befassen.

All Articles