C++ で値セマンティクスを備えたポリモーフィック コンテナーを使用できますか?

StackOverflow https://stackoverflow.com/questions/41045

  •  09-06-2019
  •  | 
  •  

質問

一般的なルールとして、私は C++ ではポインター セマンティクスよりも値を使用することを好みます (つまり、 vector<Class> の代わりに vector<Class*>)。通常、パフォーマンスのわずかな低下は、動的に割り当てられたオブジェクトを忘れずに削除する必要がなくなることで十分に補えます。

残念ながら、値コレクションは、すべて共通のベースから派生するさまざまなオブジェクト タイプを格納する場合には機能しません。以下の例を参照してください。

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

私の質問は次のとおりです。私のケーキ (値セマンティクス) を持って、それも食べることができますか (ポリモーフィック コンテナー)?それともポインタを使用する必要がありますか?

役に立ちましたか?

解決

クラスが異なればオブジェクトのサイズも異なるため、オブジェクトを値として保存するとスライスの問題が発生することになります。

合理的な解決策の 1 つは、コンテナーに安全なスマート ポインターを保存することです。私は通常、コンテナに安全に保存できる boost::shared_ptr を使用します。std::auto_ptr はそうではないことに注意してください。

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr は参照カウントを使用するため、すべての参照が削除されるまで基になるインスタンスは削除されません。

他のヒント

はい、できます。

boost.ptr_container ライブラリは、標準コンテナの多態性値セマンティック バージョンを提供します。ヒープに割り当てられたオブジェクトへのポインタを渡すだけで済みます。コンテナは所有権を取得し、所有権の再利用を除いて、その後のすべての操作で値セマンティクスが提供されます。スマート ポインタを使用すると、値セマンティクスのほぼすべての利点が得られます。 。

通常、vector<Foo> の方が Vector<Foo*> よりも効率的であることを指摘したいと思います。Vector<Foo> では、すべての Foo がメモリ内で互いに隣接します。コールド TLB とキャッシュを想定すると、最初の読み取りでページが TLB に追加され、ベクトルのチャンクが L# キャッシュにプルされます。後続の読み取りではウォーム キャッシュとロードされた TLB が使用されますが、時折キャッシュ ミスが発生し、TLB エラーの頻度は低くなります。

これをベクトル<Foo*>と比較してください。ベクターを埋めると、メモリ アロケーターから Foo* が取得されます。アロケータがそれほど賢くない (tcmalloc?) か、時間をかけてゆっくりとベクトルを埋めていくと仮定すると、各 Foo の位置は他の Foo から遠く離れている可能性があります。おそらく数百バイト、あるいはメガバイトの差かもしれません。

最悪の場合、vector<Foo*> をスキャンして各ポインターを逆参照すると、TLB フォールトとキャッシュ ミスが発生し、最終的には 多く Vector<Foo> を使用した場合よりも遅くなります。(本当に最悪の場合、各 Foo はディスクにページアウトされ、読み取りごとにページを RAM に戻すためにディスクの Seek() と read() が発生します。)

したがって、必要に応じて、vector<Foo> を使用し続けてください。:-)

ほとんどのコンテナ タイプは、リンク リスト、ベクトル、ツリーベースなど、特定のストレージ戦略を抽象化したいと考えています。このため、前述のケーキを所有することと消費することの両方に問題が生じることになります(つまり、ケーキは嘘です(注:誰かがこの冗談を言わなければならなかった))。

じゃあ何をすればいいの?いくつかのかわいいオプションがありますが、ほとんどは、いくつかのテーマの 1 つまたはそれらの組み合わせのバリエーションになります。適切なスマート ポインタを選択または発明し、コンテナごとの二重ディスパッチを実装するためのフックを提供するコンテナ用の共通インターフェイスを使用して、何らかの賢い方法でテンプレートまたはテンプレート テンプレートを操作します。

定められた 2 つの目標の間には基本的な緊張関係があるため、何を望むかを決めてから、基本的に望むものを実現するものをデザインするように努める必要があります。それ 十分に賢明な参照カウントと十分に賢明なファクトリの実装を使用して、値のように見えるポインタを取得するいくつかの素晴らしい予想外のトリックを実行することが可能です。基本的な考え方は、参照カウント、コピーオンデマンド、定数性、および (要素として) プリプロセッサ、テンプレート、および C++ の静的初期化ルールの組み合わせを使用して、ポインタ変換の自動化について可能な限り賢明なものを実現することです。

私は過去に、C++ での値セマンティック プログラミングの基礎のようなものを達成するために、仮想プロキシ / エンベロープ レター / 参照カウント ポインターを使用したそのかわいいトリックを使用する方法を構想するのに時間を費やしました。

それは可能だと思いますが、C++ 内にかなり閉じた C# マネージ コードのような世界を提供する必要があります (ただし、必要に応じてそこから基盤となる C++ にブレークスルーできる世界)。だから私はあなたの考え方にとても共感します。

検討することもできます ブースト::任意. 。異種コンテナに使用しました。値を読み取るときは、any_cast を実行する必要があります。失敗すると bad_any_cast がスローされます。そうなれば、捕まえて次のタイプに移ることができます。

信じる 派生クラスをそのベースに any_cast しようとすると、 bad_any_cast がスローされます。私はそれを試してみました:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

そうは言っても、私も最初はshared_ptrルートを選択します。これは興味深いかもしれないと思いました。

を見てみましょう static_cast そして 再解釈_キャスト
『C++ プログラミング言語、第 3 版』では、Bjarne Stroustrup が 130 ページで説明しています。これに関するセクション全体が第 6 章にあります。
親クラスを子クラスに再キャストできます。これには、それぞれがいつなのかを知る必要があります。本の中で、Dr.ストルストラップ氏は、この状況を回避するためのさまざまなテクニックについて語ります。

こんなことしないで。これは、そもそも達成しようとしているポリモーフィズムを無効にします。

すべてに 1 つだけ追加する 1800 情報 すでに言いました。

見てみるのもいいかもしれません 「より効果的な C++」 スコット・メイヤーズ著「項目 3:この問題をよりよく理解するには、配列を多態的に扱わないでください。」

値型セマンティクスを公開した独自のテンプレート化されたコレクション クラスを使用していますが、内部的にはポインターを格納します。逆参照時にポインタの代わりに値参照を取得するカスタム反復子クラスを使用しています。コレクションをコピーすると、ポインターが複製されるのではなく、アイテムの深いコピーが作成されます。これが、ほとんどのオーバーヘッドが発生する場所です (本当に小さな問題ですが、代わりに得られるものと考えられます)。

それはあなたのニーズに応えられるアイデアです。

この問題に対する答えを探しているときに、これと 同様の質問. 。他の質問への回答には、次の 2 つの解決策が提案されています。

  1. std::optional または boost::optional と訪問者パターンを使用します。このソリューションでは、新しい型を追加するのは難しくなりますが、新しい機能を追加するのは簡単になります。
  2. 次のようなラッパー クラスを使用します。 ショーン・ペアレント氏の講演. 。このソリューションでは、新しい機能を追加するのは難しくなりますが、新しいタイプを追加するのは簡単になります。

ラッパーはクラスに必要なインターフェイスを定義し、そのようなオブジェクトへのポインターを保持します。インターフェースの実装は無料の関数を使用して行われます。

このパターンの実装例を次に示します。

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

その後、さまざまな形状が通常の構造体/クラスとして実装されます。メンバー関数を使用するか無料関数を使用するかを自由に選択できます (ただし、メンバー関数を使用するには上記の実装を更新する必要があります)。私は無料の機能を好みます:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

両方を追加できるようになりました Circle そして Rectangle 同じものに反対する std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

このパターンの欠点は、各関数を 3 回定義する必要があるため、インターフェイスに大量の定型文が必要になることです。利点は、コピー セマンティクスが得られることです。

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

これにより、以下が生成されます。

Drew rectangle with width 2
Drew rectangle with width 2

心配な場合は、 shared_ptr, に置き換えることができます。 unique_ptr。ただし、コピーできなくなるため、すべてのオブジェクトを移動するか、手動でコピーを実装する必要があります。Sean Parent は講演の中でこれについて詳しく説明しており、実装は上記の回答に示されています。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top