私は最近、共有したい型消去についての洞察を得ました。型消去は、ポリモーフィズムと値セマンティクスの両方を実現するために連携する 2 つの手法の組み合わせです。 、提案された標準ライブラリ タイプ、およびダック タイピング。
ビジター パターンの投稿で使用した例をもう一度見てみましょう。Markdown などのマークアップ言語の AST をモデル化したいと考えています。テキスト、強調、コード ブロックなどが含まれています。入力を解析し、 AST を HTML に変換する必要があります。
それをモデル化するための自然なアプローチは、クラス階層を使用することです:16
があります 基本クラスと 28
などの派生クラス 、 31
、 47
、 50
など。いくつかのクラスは、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
?
さて、与えられた 440
、 453
、 463
などは 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
のようなものを使用することを検討してください 代わりに、ボイラープレートがまったくなくても多くのメリットが得られます。