6

古典的な仮想継承のダイアモンド階層を考えてみましょう。そのような階層でのコピーとスワップのイディオムの正しい実装は何であるかを知りたいと思います。

このは、A、B、Dクラスのデフォルトのコピーセマンティクスでうまく機能するため、少し人工的であり、あまりスマートではありません。ただし、問題を説明するためだけに、弱点の例を忘れて解決策を提供してください。

したがって、2つの基本クラス(B <1>、B <2>)から派生したクラスDがあります。各Bクラスは、実質的にAクラスから継承します。各クラスには、コピーとスワップのイディオムを使用した、自明ではないコピーセマンティクスがあります。最も派生したDクラスには、このイディオムの使用に問題があります。B<1>およびB<2>スワップメソッドを呼び出すと(仮想基本クラスメンバーを2回スワップします)、サブオブジェクトは変更されません!!!

A:

class A {
public:
  A(const char* s) : s(s) {}
  A(const A& o) : s(o.s) {}
  A& operator = (A o)
  {
     swap(o);
     return *this;
  }
  virtual ~A() {}
  void swap(A& o)
  {
     s.swap(o.s);
  }
  friend std::ostream& operator << (std::ostream& os, const A& a) { return os << a.s; }

private:
  S s;
};

B

template <int N>
class B : public virtual A {
public:
  B(const char* sA, const char* s) : A(sA), s(s) {}
  B(const B& o) : A(o), s(o.s) {}
  B& operator = (B o)
  {
     swap(o);
     return *this;
  }
  virtual ~B() {}
  void swap(B& o)
  {
     A::swap(o);
     s.swap(o.s);
  }
  friend std::ostream& operator << (std::ostream& os, const B& b) 
  { return os << (const A&)b << ',' << b.s; }

private:
  S s;
};

D:

class D : public B<1>, public B<2> {
public:
  D(const char* sA, const char* sB1, const char* sB2, const char* s) 
   : A(sA), B<1>(sA, sB1), B<2>(sA, sB2), s(s) 
  {}
  D(const D& o) : A(o), B<1>(o), B<2>(o), s(o.s) {}
  D& operator = (D o)
  {
     swap(o);
     return *this;
  }
  virtual ~D() {}
  void swap(D& o)
  {
     B<1>::swap(o); // calls A::swap(o); A::s changed to o.s
     B<2>::swap(o); // calls A::swap(o); A::s returned to original value...
     s.swap(o.s);
  }
  friend std::ostream& operator << (std::ostream& os, const D& d) 
  { 
     // prints A::s twice...
     return os 
    << (const B<1>&)d << ',' 
    << (const B<2>&)d << ',' 
        << d.s;
  }
private:
  S s;
};

S文字列を格納する単なるクラスです。

コピーを実行すると、A::sは変更されないままになります。

int main() {
   D x("ax", "b1x", "b2x", "x");
   D y("ay", "b1y", "b2y", "y");
   std::cout << x << "\n" << y << "\n";
   x = y;
   std::cout << x << "\n" << y << "\n";
}

そして結果は次のとおりです。

ax,b1x,ax,b2x,x
ay,b1y,ay,b2y,y
ax,b1y,ax,b2y,y
ay,b1y,ay,b2y,y

おそらく追加B<N>::swapOnlyMeすると問題が解決します:

void B<N>::swapOnlyMe(B<N>& b) { std::swap(s, b.s); }
void D::swap(D& d) { A::swap(d); B<1>::swapOnlyMe((B<1>&)d); B<2>::swapOnlyMe((B<2>&)d); ... }

しかし、BがAから個人的に継承する場合はどうなりますか?

4

2 に答える 2

7

ここに哲学的な暴言があります:

  1. 仮想継承は非公開にすることも、非公開にする必要もないと思います。仮想ベースの要点は、中間クラスではなく、最も派生したクラスが仮想ベースを所有することです。したがって、中間クラスが仮想ベースを「独り占め」することは許可されるべきではありません。

  2. 繰り返しますが、最も派生したクラスが仮想ベースを所有しています。これは、コンストラクターの初期化子で明らかです。

    D::D() : A(), B(), C() { }
    //       ^^^^
    //       D calls the virtual base constructor!
    

    同じ意味で、 の他のすべての操作は、Dに対して直ちに責任を負う必要がありますA。したがって、当然、次のような派生スワップ関数を書くことになります。

    void D::swap(D & rhs)
    {
        A::swap(rhs);   // D calls this directly!
        B::swap(rhs);
        C::swap(rhs);
    
        // swap members
    }
    
  3. これらすべてをまとめると、考えられる結論は 1 つだけです。ベースをスワップせずに中間クラスのスワップ関数を作成する必要があります。

    void B::swap(B & rhs)
    {
        // swap members only!
    }
    
    void C::swap(C & rhs)
    {
        // swap members only!
    }
    

ここで、「他の誰かが から派生させたい場合はどうすればよいDでしょうか? これで、非リーフ クラスを常に抽象化するという Scott Meyer のアドバイスの理由がわかります。そのアドバイスに従って、仮想ベース スワップを呼び出す最終関数のみswapを実装します。具体的な葉のクラス。


更新:接線的にのみ関連するものがあります: 仮想スワッピング。すべての非リーフ クラスは抽象クラスであると引き続き想定しています。まず、次の「仮想スワップ関数」をすべての基本クラス (仮想かどうかに関係なく) に入れます。

struct A
{
    virtual void vswap(A &) = 0;
    // ...
};

もちろん、この関数の使用は同一の型にのみ予約されています。これは、暗黙の例外によって保護されています。

struct D : /* inherit */
{
    virtual void vswap(A & rhs) { swap(dynamic_cast<D &>(rhs)); }

    // rest as before
};

これの全体的な有用性は限られていますが、オブジェクトが同じであることがわかっている場合は、多態的にオブジェクトにスワップできます。

std::unique_ptr<A> p1 = make_unique<D>(), p2 = make_unique<D>();
p1->vswap(*p2);
于 2012-09-07T09:51:42.900 に答える
1

仮想ベースとは、一般に、オブジェクトのほとんどの派生クラスがそれを制御していることを意味します。

最初の解決策:クラスを再編成して、ポリモーフィズムにより適したものにします。コピー作成を保護します。割り当てと を削除しswap()ます。仮想を追加しますclone()。アイデアは、クラスをポリモーフィックとして扱う必要があるということです。そのため、ポインターまたはスマート ポインターと共に使用する必要があります。スワップまたは割り当ては、オブジェクト値ではなく、ポインター値である必要があります。そのようなコンテキストでは、スワップと代入は混乱するだけです。

2 番目の解決策: B と C を抽象化し、それらのポインターをオブジェクトの有効期間を管理しないようにします。B と C のデストラクタは保護され、非仮想である必要があります。したがって、B と C はオブジェクトのほとんどの派生クラスにはなりません。B::swap()Aサブオブジェクトを作成して保護し、スワップC::swap()しないと、名前を変更したり、継承されたクラスのビジネスであるというコメントを追加したりできます。これにより、多くのオブジェクト スライスの可能性が排除されます。Make D::swap()to swap A サブオブジェクト。A のスワップを 1 つ取得します。

3 番目の解決策: A サブオブジェクトD::swap()を交換するようにします。そうすれば、A サブオブジェクトが 3 回スワップされ、正しい場所に着地します。非効率的な?とにかく、全体の構成はおそらく悪い考えです。たとえば、ここで仮想デストラクタとスワップがどの程度うまく連携しているかはわかりません。また、オブジェクトをスライスする多くの方法がここで公開されています。それはすべて、C++ で悪い考えである仮想代入演算子を作成しようとする試みに似ているようです。

何かがその順序で D から継承する場合、A サブオブジェクトを交換するか交換しないかによって、A を交換する回数が奇数であることを確認する必要があります。制御するようになるので、引き継いで修正する必要があります。

private virtualイディオムは、C++ でクラスを final にする方法の 1 つです。そこから何も継承できないはずです。あなたが尋ねたことは興味深い。これを使用する場合は、必ずコメントしてください。コードの読者のほとんどを混乱させます。

于 2012-09-07T10:28:27.077 に答える