std::polymorphic_value + Duck Typing =Type Erasure

私は最近、共有したい型消去についての洞察を得ました。型消去は、ポリモーフィズムと値セマンティクスの両方を実現するために連携する 2 つの手法の組み合わせです。 、提案された標準ライブラリ タイプ、およびダック タイピング。

ビジター パターンの投稿で使用した例をもう一度見てみましょう。Markdown などのマークアップ言語の AST をモデル化したいと考えています。テキスト、強調、コード ブロックなどが含まれています。入力を解析し、 AST を HTML に変換する必要があります。

それをモデル化するための自然なアプローチは、クラス階層を使用することです:16 があります 基本クラスと 28 などの派生クラス 、 314750 など。いくつかのクラスは、68 のように、子ノードのコンテナーです。 、 79 のようにそうでないものもあります .

class node
{ 
public:
    virtual ~node() = default;
    virtual std::string render_html() const = 0;
};

class text final : public node
{
public:
    std::string render_html() const override
    {
        return sanitize_html(content_);
    }

private:
    std::string content_;
};

class document final : public node
{
public:
    std::string render_html() const override
    {
        std::string result = "<head>…</head>\n<body>\n";
        for (auto& child : children_)
            result += child->render_html(); 
        result += "</body>\n";
        return result;
    }

private:
    std::vector<std::unique_ptr<node>> children_;
};

…

これは十分に機能し、私が標準語で行ったことと似ています。

ただし、気に入らない点が 2 つあります。

問題:価値のセマンティクスの欠如

Scott Meyers はかつて、「int と同じように」すべきだと言いました。84 のように動作するクラスを作成します。 s.この言語は 91 での作業を非常に便利にするため、これは非常に理にかなっています。 :スタック上にそれらを作成し、それらを渡し、完全に別のエンティティであるコピーを作成し、 101 を含むクラスにすることができます s はゼロの規則などに従うことができます。

int do_something(int a, int b)
{
    int tmp = a + b;
    int copy = tmp;
    ++tmp;
    // copy is unaffected
    return tmp + copy;
}

110 など、ほとんどの標準ライブラリ クラスはこのアドバイスに従います。 .そのため、同じ原則がすべて適用されます:

std::string do_something(std::string a, std::string b b)
{
    std::string tmp = a + b;
    std::string copy = tmp;
    tmp += "world";
    // copy is unaffected
    return tmp + copy;
}

組み込み型のように動作するクラスを作成できるこの機能は、C++ の最も重要な機能の 1 つです。

ただし、クラス階層はこのようには動作しません! 123 から派生した型を保持する変数を作成することはできません スタックでは、それをヒープに配置する必要があり、メモリ管理が必要です。単にそれらを渡す (スライスする) だけではなく、参照または (スマート) ポインターを渡す必要があります。それらをコピーして取得することはできません別のエンティティ、せいぜい参照カウントを行うことができます。 133 の任意の派生型を含むクラス 、 147 のように 、実際のオブジェクトへのポインターまたは参照の有効期間管理の負担が増えるため、ゼロの規則に従うことはできません。たとえば、 151 の独自のコピー コンストラクターを記述する必要があります。 .

166 と同じように振る舞うとより良いでしょう。

ソリューション:値セマンティック ラッパー

もちろん、間接的なレイヤーを追加することで、この問題を解決できます。手動で 177 を渡す代わりに 186 を作成します ヒープに割り当てられた 198 を格納します 、しかしそれをラップし、値のセマンティクスを提供します。

最も基本的なレベルでは、209 が含まれているだけです。 もう一度:

class node_value
{
public:
    template <typename T>
      requires std::is_base_of_v<node, T>
    node_value(T obj)
    : ptr_(std::make_unique<T>(std::move(obj))
    {}

    node* operator->() const
    {
        return ptr_.get();
    }
    node& operator*() const
    {
        return *ptr_;
    }

private:
    std::unique_ptr<node> ptr_;
};

212 から派生した任意のオブジェクトを取るコンストラクターがあります。 (226 によって制約されます )そしてそれをヒープに置きます。次に、ノードを提供するアクセスのようなポインターを提供します。これまでのところ、これはプレーンな 236 と変わりません

トリックは、 248 を追加すると、コピー コンストラクターを記述できるようになることです。 クラス階層への関数:

class node
{ 
public:
    virtual std::unique_ptr<node> clone() const = 0;
};

class text final : public node
{
public:
    std::unique_ptr<node> clone() const override
    {
        return std::make_unique<text>(content_);
    }

private:
    std::string content_;
};

class document final : public node
{
public:
    std::unique_ptr<node> clone() const override
    {
        std::vector<std::unique_ptr<node>> children;
        for (auto& c : children_)
            children_.push_back(c->clone());
        return std::make_unique<document>(std::move(children));
    }


private:
    std::vector<std::unique_ptr<node>> children_;
};

…

この 250 関数は基本的に 269 です コピー コンストラクター。次に、275 のコピーを実装できます。 :

class node_value
{
public:
    node_value(const node_value& other)
    : ptr_(other->clone())
    {}

    node_value& operator=(const node_value& other)
    {
        ptr_ = other->clone();
        return *this;
    }

private:
    std::unique_ptr<node> ptr_;
};

そして今、 288 ながら まだ 291 のようには動作しません s、305 する:スタック上で自由に作成したり、コピーしたりできます。値のセマンティクスを提供しない型を提供する型にラップしましたが、定型的なコストがかかります.

幸いなことに、基本的に一般的な 319 の提案があります。 :328 .A 336 342 とまったく同じように動作します .

std::polymorphic_value<node> n = …;
auto html = n->render_html();

std::polymorphic_value<node> copy = n;
…

354 を必要とせずに正しいコピーを実行することさえできます メンバー関数!ここで参照実装を見つけることができます:github.com/jbcoe/polymorphic_value.

問題:暗黙的な拡張性がない

364 の 2 つ目の問題 クラス階層は、すべてのクラス階層に共通のものです。基本クラスに参加するには、基本クラスを認識する必要があります。

サードパーティのライブラリがたまたまクラスに 373 を提供した場合はどうなるでしょうか 関数? 384 から派生していないため、使用できません .

解決策:ダックタイピング

391 を提供する任意のオブジェクトを取るラッパーを提供することで解決できます。 関数ですが、400 から継承します :

template <typename T>
class node_like final : public node
{
public:
    node_like(T obj)
    : obj_(std::move(obj))
    {}

    // We can provide cloning by simply using T's copy constructor,
    // if it is still required.
    std::unique_ptr<node> clone() const override
    {
        return std::make_unique<node_like<T>>(obj_); 
    }

    std::string render_html() const override
    {
        return obj_.render_html();
    }

private:
    T obj_;
};

そうすれば、任意の型を 411 の一部にすることができます

組み合わせ:タイプ消去

427 を組み合わせるとどうなるか と 430 ?

さて、与えられた 440453463 などは 474 から継承する必要はありません 482 でラップするだけです。 .そして、497 のみを保存するため 500 の 、すべてのラッピングを任せることができます:

class node_value
{
public:
    template <typename T>
    node_value(T obj)
    : ptr_(std::make_unique<node_like<T>>(std::move(obj)))
    {}

    // dereference and copy as before

private:
    std::unique_ptr<node> ptr_;
};

この時点で、519 526 を提供する任意のタイプを処理できます function.さて、本当に 532 を保持する必要がありますか? 基本クラスまたは 543 public?任意の 557 で動作する関数 s は 562 を取ることができます 、および 573 586 が必要とする単なるラッパーです .

したがって、さらに一歩進んで、598 の 2 つのクラスの実装の詳細を作成できます。 .これにより、名前 602 も解放されます 、だから 614 の名前を変更できます 単に 622 に .逆参照を提供する代わりに、インターフェイス 639 を手動で実装するだけです もともと持っていた - 644 でできることだから とにかく!

class node // formerly node value
{
    class base // formerly node
    {
    public:
      virtual ~base() = default;
      virtual std::unique_ptr<base> clone() const = 0;
      virtual std::string render_html() const = 0;
    };

    template <typename T>
    class wrapper final : public base // formely node_like
    {
    public:
        wrapper(T obj)
        : obj_(std::move(obj))
        {}

        std::unique_ptr<base> clone() const override
        {
            return std::make_unique<wrapper<T>>(obj_); 
        }
        std::string render_html() const override
        {
            return obj_.render_html();
        }

    private:
        T obj_;
    };

public:
    template <typename T>
    node(T obj)
    : ptr_(std::make_unique<wrapper<T>>(std::move(obj)))
    {}

    node(const node& other)
    : ptr_(other.ptr_->clone())
    {}

    node& operator=(const node& other)
    {
        ptr_ = other.ptr_->clone();
        return *this;
    }

    std::string render_html() const
    {
        return ptr_->render_html();
    }

private:
    std::unique_ptr<base> ptr_;
};

今度は 659 そして 668 クラスは 674 を持つ通常のクラスです 関数:

class text 
{
public:
    std::string render_html() const
    {
        return sanitize_html(content_);
    }

private:
    std::string content_;
};

class document
{
public:
    std::string render_html() const
    {
        std::string result = "<head>…</head>\n<body>\n";
        for (auto& child : children_)
            result += child.render_html(); 
        result += "</body>\n";
        return result;
    }

private:
    std::vector<node> children_;
};

何かから継承する必要はなく、他の 680 を保存する必要もありません s in a pointer、copy はすぐに使用できます。

値セマンティクス ラッパーとダック タイピングを組み合わせることで、スマート ポインターの厄介な使用を必要とするクラス階層がなくなり、代わりに値セマンティクスを持つ単純な型が使用されます。必要な機能を備えています。これにより、拡張が非常に簡単になります。

この手法は型消去です。ポリモーフィックな動作、値のセマンティクス、ダックタイピングを組み合わせたものです。694 型消去を使用します。必要なインターフェースは 701 です (およびコピー コンストラクター).712 型消去も提供します。コピー コンストラクタとデストラクタのみが必要です。さらに 727 も必要です。 コピーを提供するために型消去を行います。

ボイラープレートを型消去する唯一の欠点:必要な仮想関数を含む基本クラス、転送するだけのテンプレート化されたラッパー、および基本クラスに転送するパブリック インターフェイスを作成する必要があります。これは面倒です。ただし、型がメタ プログラミング技術を使用して定型文の多くを排除するライブラリもあります.メタクラスはそれを完全に排除することさえできます.

また、型消去を使用しない場合でも、 732 のようなものを使用することを検討してください 代わりに、ボイラープレートがまったくなくても多くのメリットが得られます。