デコレータのコンパイル方法-C ++、Python、および独自の実装。パート1

この一連の記事では、C ++でデコレーターを作成する可能性、Pythonでの作業の機能に焦点を当て、独自のコンパイル済み言語でこの機能を実装するためのオプションの1つについて、クロージャーを作成するための一般的なアプローチ(クロージャー変換と構文ツリーの近代化)を使用して検討します。



免責事項
, Python — . Python , (). - ( ), - ( , ..), Python «» .

C ++のデコレーター


それはすべて、私の友人であるVoidDruidが小さなコンパイラを卒業証書として書くことに決めたという事実から始まりました。その主な機能はデコレータです。防御前でも、彼がASTの変更を含む彼のアプローチのすべての利点を概説したとき、私は不思議に思っていました:偉大で強力なC ++でこれらの同じデコレーターを実装し、複雑な用語やアプローチなしで行うことは本当に不可能ですか?このトピックをグーグルで調べたところ、この問題を解決するための単純で一般的なアプローチは見つかりませんでした(ところで、デザインパターンの実装に関する記事だけに出くわしました)。その後、自分のデコレーターを書き始めました。


ただし、実装の直接の説明に移る前に、C ++のラムダとクロージャーがどのように配置されているか、およびそれらの違いについて少しお話したいと思います。特定の標準についての言及がない場合は、デフォルトでC ++ 20を意味することをすぐに予約してください。つまり、ラムダは匿名関数であり、クロージャは環境のオブジェクトを使用する関数です。たとえば、C ++ 11以降では、ラムダを次のように宣言して呼び出すことができます。

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

または、その値を変数に割り当て、後で呼び出します。

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

しかし、コンパイル中に何が起こり、ラムダとは何ですか?ラムダの内部構造に没頭するには、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);
    }    
};


したがって、コンパイルすると、ラムダはクラス、またはファンクタ(演算子()が定義されているオブジェクト)に変わります。これは、ラムダに渡されたパラメータを受け入れる演算子()とその本体を含む、自動的に生成された一意の名前を持ちますラムダが実行する必要のあるコード。これですべてが明確になりましたが、他の2つの方法はどうですか、なぜですか?1つ目は関数ポインターへのキャストの演算子であり、そのプロトタイプはラムダと一致します。2つ目は、ラムダがポインターへの事前割り当て時に呼び出されたときに実行される必要があるコードです。たとえば、次のようになります。

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

さて、謎は1つ減りますが、閉鎖についてはどうでしょうか。変数 "a"を参照で取得して1だけ増やすクロージャの最も簡単な例を書いてみましょう。

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

ご覧のとおり、C ++でクロージャとラムダを作成するメカニズムはほとんど同じなので、これらの概念はしばしば混乱し、ラムダとクロージャは単にラムダと呼ばれます。

しかし、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 ++のラムダとクロージャーの概要を理解したところで、直接デコレーターを作成します。ただし、最初に、要件を決定する必要があります。

したがって、デコレータは関数またはメソッドを入力として受け取り、それに必要な機能を追加し(たとえば、これは省略されます)、呼び出されたときに新しい関数を返し、コードと関数/メソッドコードが実行されます。この時点で、自尊心のあるパイソン主義者はこう言うでしょう。デコレータは元のオブジェクトを置き換える必要があり、名前でそれを呼び出すと、新しい関数を呼び出す必要があります。これがC ++の主な制限であり、ユーザーが古い関数を呼び出すのを止めることはできません。もちろん、メモリ内でそのアドレスを取得してグラインドする(この場合、アクセスするとプログラムが異常終了する)オプション、または本体でコンソールで使用してはならないという警告に置き換えるオプションがありますが、これには深刻な結果が伴います。最初のオプションがまったく難しいと思われる場合は、次に、さまざまなコンパイラ最適化を使用すると、2番目もクラッシュする可能性があるため、使用しません。また、ここでマクロマジックを使用することは冗長であると考えています。

それでは、デコレーターの作成に移りましょう。私の頭に浮かんだ最初のオプションはこれです:

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

この特定のオプションの利点は、演算子()を持つすべてのオブジェクトを完全に装飾できることです。したがって、たとえば、フリー関数、ポインター、ラムダ、任意のファンクター、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を使用するための拡張機能があり、std :: __ invokeで置き換える必要がある場合、C ++ 14でコンパイルできます。拡張がない場合、クラスメソッドを装飾する機能を放棄する必要があり、この機能は使用できなくなります。

煩雑な「std :: forward <decltype(args)>(args)...」を記述しないようにするために、C ++ 20で利用可能な機能を使用して、ラムダのボイラープレートを作成できます。

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