考虑经典的虚拟继承钻石层次结构。我想知道在这种层次结构中复制和交换习语的正确实现是什么。
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私下继承时会怎样?
答案 0 :(得分:6)
这是一个哲学咆哮:
我不认为虚拟继承可以或应该是私有的。虚拟基础的全部要点是大多数派生类拥有虚拟基础,而不是中间类。因此,不应允许任何中间类“占用”虚拟基地。
让我重复一点:最派生的类拥有虚拟基础。这在构造函数初始化器中很明显:
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
}
把所有这些放在一起,我们只剩下一个可能的结论:你必须编写中间类的交换函数而不需要交换基数:
void B::swap(B & rhs)
{
// swap members only!
}
void C::swap(C & rhs)
{
// swap members only!
}
现在你问,“如果有人想从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 ++中使类成为最终的方法之一。没有什么能够继承它。有趣的是你问。如果您曾经使用它,请务必发表评论,这会使大多数代码读者感到困惑。