在虚拟继承中交换和复制习惯用法的正确方法是什么?

时间:2012-09-07 08:53:40

标签: c++ multiple-inheritance virtual-inheritance diamond-problem copy-and-swap

考虑经典的虚拟继承钻石层次结构。我想知道在这种层次结构中复制和交换习语的正确实现是什么。

example有点人为 - 并且它不是很聪明 - 因为它可以很好地使用A,B,D类的默认复制语义。但只是为了说明问题 - 请忘记示例弱点并提供解决方案。

因此,我得到了来自2个基类(B 1,B 2)的D类 - 每个B类实际上都是从A类继承的。每个类都有非平凡的复制语义,使用复制和交换习语。派生最多的D类在使用这个习语时有问题。当它称为B< 1>时。和B 2和B 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;
};

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私下继承时会怎样?

2 个答案:

答案 0 :(得分:6)

这是一个哲学咆哮:

  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. 把所有这些放在一起,我们只剩下一个可能的结论:你必须编写中间类的交换函数而不需要交换基数:

    void B::swap(B & rhs)
    {
        // swap members only!
    }
    
    void C::swap(C & rhs)
    {
        // swap members only!
    }
    
  4. 现在你问,“如果有人想从D派生出来怎么办?现在我们看到Scott Meyer的建议总是让非叶子类抽象化的原因:遵循这个建议,你只< / em>实现最终的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);
    

答案 1 :(得分:1)

虚拟基础通常意味着大多数派生类的对象都在控制它。

第一个解决方案:重新组织您的类以更适合多态。使复制结构受到保护。删除作业和swap()。添加虚拟clone()。想法是应该将类视为多态。所以它们应该与指针或智能指针一起使用。 Swapped或assign应该是指针值,而不是对象值。在这种情况下,交换和分配只会混淆。

第二个解决方案:使B和C抽象,他们的指针不管理对象的生命周期。 B和C的析构函数应该受到保护并且是非虚拟的。因此,B和C不是大多数派生类的对象。使B::swap()C::swap()受保护而不交换A子对象,可以重命名或添加它现在是继承类业务的注释。这消除了许多对象切片的可能性。使D::swap()交换A子对象。你得到一次A的交换。

第三个解决方案:D::swap()交换一个子对象。这样,A子对象将被交换3次并落在正确的位置。效率低下?无论如何,整个构造可能都是坏主意。例如,我不确定虚拟析构函数和交换在这里是如何合作的,在这里公开切片对象的方法很多。这一切看起来类似于尝试在C ++中制作虚拟赋值运算符。

如果某个东西继承自D的顺序,那么它应该通过交换或不交换一个子对象来确保交换A的计数是奇数。它变成了控制所以应该​​接管并修复。

private virtual成语是在C ++中使类成为最终的方法之一。没有什么能够继承它。有趣的是你问。如果您曾经使用它,请务必发表评论,这会使大多数代码读者感到困惑。