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

Pio*_*ycz 4 c++ multiple-inheritance virtual-inheritance diamond-problem copy-and-swap

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

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

所以我从两个基类(B <1>,B <2>)派生出D类 - 每个B类几乎从A类继承.每个类都有非平凡的复制语义,使用复制和交换习语.派生最多的D类在使用这个习语时有问题.当它调用B <1>和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;
};
Run Code Online (Sandbox Code Playgroud)

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;
};
Run Code Online (Sandbox Code Playgroud)

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;
};
Run Code Online (Sandbox Code Playgroud)

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";
}
Run Code Online (Sandbox Code Playgroud)

结果是:

ax,b1x,ax,b2x,x
ay,b1y,ay,b2y,y
ax,b1y,ax,b2y,y
ay,b1y,ay,b2y,y
Run Code Online (Sandbox Code Playgroud)

可能添加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); ... }
Run Code Online (Sandbox Code Playgroud)

但是当B从A私下继承时呢?

Ker*_* SB 6

这是一个哲学咆哮:

  1. 我不认为虚拟继承可以或应该是私有的.虚拟基础的整个要点是,派生程度最高的类拥有虚拟基础,而不是中间类.因此,不应允许任何中间类"占用"虚拟基础.

  2. 让我重复一点:最派生的类拥有虚拟基础.这在构造函数初始化器中很明显:

    D::D() : A(), B(), C() { }
    //       ^^^^
    //       D calls the virtual base constructor!
    
    Run Code Online (Sandbox Code Playgroud)

    在同样的意义上,所有其他操作D应该立即负责A.因此,我们自然会像这样编写派生交换函数:

    void D::swap(D & rhs)
    {
        A::swap(rhs);   // D calls this directly!
        B::swap(rhs);
        C::swap(rhs);
    
        // swap members
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 把所有这些放在一起,我们只剩下一个可能的结论:你必须编写中间类的交换函数而不需要交换基数:

    void B::swap(B & rhs)
    {
        // swap members only!
    }
    
    void C::swap(C & rhs)
    {
        // swap members only!
    }
    
    Run Code Online (Sandbox Code Playgroud)

现在你问,"如果其他人想要从中得到D什么呢?现在我们看到Scott Meyer的建议总是使非叶类抽象的原因:遵循这个建议,你实现swap调用虚拟基础交换的最终函数混凝土,叶子类.


更新:这里只是切向相关的东西:虚拟交换.我们继续假设所有非叶类都是抽象的.首先,我们将以下"虚拟交换功能"放入每个基类(虚拟或非虚拟):

struct A
{
    virtual void vswap(A &) = 0;
    // ...
};
Run Code Online (Sandbox Code Playgroud)

此功能的使用当然仅保留给相同类型.这是由隐式异常保护的:

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

    // rest as before
};
Run Code Online (Sandbox Code Playgroud)

这种方法的整体效用是有限的,但如果我们碰巧知道它们是相同的,它确实允许我们以多态方式交换对象:

std::unique_ptr<A> p1 = make_unique<D>(), p2 = make_unique<D>();
p1->vswap(*p2);
Run Code Online (Sandbox Code Playgroud)