6

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

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

所以我有从 2 个基类 (B<1>,B<2>) 派生的 D 类 - 每个 B 类实际上都继承自 A 类。每个类都具有使用复制和交换习语的非平凡复制语义。最派生的 D 类在使用这个习语时有问题。当它调用 B<1> 和 B<2> 交换方法时 - 它交换虚拟基类成员两次 - 所以 A 子对象保持不变!!!

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;
};

丁:

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

    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(). 想法是类应该被视为多态的。所以它们应该与指针或智能指针一起使用。交换或分配的应该是指针值,而不是对象值。在这种情况下,交换和分配只会混淆。

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

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

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

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

于 2012-09-07T10:28:27.077 回答