为C++中具有多个继承派生类的vtables的基之一调用赋值运算符

Calling an assignment operator for one of bases with vtables of multiple-inherited derived class in C++

本文关键字:vtables 赋值运算符 调用 派生 C++ 继承      更新时间:2023-10-16

好吧,这会有点棘手。这是一个(简化的)代码:

class A
{
    virtual ~A();
    // fields, none of which has an assignment operator or copy constructor
};
class B
{
    virtual ~B();
    // same as A
};
class Derived : public A, public B
{
    Derived();
    Derived(const B& b);
    // no fields
};

对于Derived::Derived(const B& b)(即接受其碱基之一),如下

Derived::Derived(const B& b)
{
    *static_cast<B*>(this) = b;
    // Do other stuff with protected fields declared in B
}

对我来说,这是一种"避免这样做"的行为,但这是一个现有的代码,我们在这个代码附近经历了一个微妙的内存损坏,令人怀疑。所以,我很好奇这是否可以。

奇怪的是,这两个基类都有vtables,并且没有任何显式的复制/赋值构造函数/运算符。

根据我的理解,Derived类的内存布局如下

 `Derived`
 ---------
 A-vtable
 A-fields
 B-vtable
 B-fields

当我调用一个在"B"中声明的虚拟函数时,我使用的是B-vtable,当我调用在"a"中宣布的虚拟函数,我使用A-vtable,即vtables不会合并在一起。

根据我的理解,当B被调用为*static_cast<B*>(this) = b;时,它的隐式复制/赋值构造函数/运算符应该只影响B-fieldsstatic_cast应该给出一个指向B-fields开头的指针,B-vtable位于它的负偏移量AFAIK)。

因此,根据我的理解,这个代码是完全安全和正确的,虽然不清楚和黑客,但安全。我说得对吗?有什么编译器特有的怪癖我应该注意吗(我们在这里谈论的是MSVC 2012)?

编辑:伙计们,我知道复制构造函数/赋值运算符的区别,非常感谢。这是三次谈论复制构造函数的事件之一,因为我已经监督过它,现在每个答案都花了一半的时间来告诉完全没有问题的人。

是的,它是正确的。有一种特殊的行为,将派生类强制转换为其父类之一。当发生多重继承时,就像您的情况一样,指针的实际地址可能会发生变化。举个例子:

this                     ->   | A-vtable |
                              | A-fields |
static_cast<B*>(this)    ->   | B-vtable |
                              | B-fields |

当您在Derived对象上调用从B派生的函数时,也会发生同样的指针变化。


但是,请注意,复制构造函数是而不是您在以下行中调用的:

*static_cast<B*>(this) = b;

相反,您调用的是B的赋值运算符,即B::operator=(const B& other)。如果没有定义,则使用默认的赋值运算符。等号仅在变量声明的上下文中被视为复制构造函数:

B newObj = b;

如果您想为Derived实现自己的复制构造函数,然后显式调用B的父复制构造函数,请尝试以下操作:

Derived::Derived(const B& b) : B(b)
{
}

为什么不直接调用基类的构造函数?

Derived::Derived(const B& b)
: B(b)
{
}

分配操作员也可以做类似的事情:

Deriver& Derived::operator=(const Derived& rhs)
{
    A::operator=(rhs); //Assign A's part
    B::operator=(rhs); //Assign B's part
    //Derived-specific assignments
    return *this;
}

这是初始化类的对象的首选、安全且完全有效的方法,这些对象继承自某个对象。

此外,在这一行中:

*static_cast<B*>(this) = b;

您调用赋值运算符,而不是复制构造函数。由于您编写了,两个基类型都没有定义,因此使用默认值(由编译器生成)。

我不明白你为什么还要考虑vtables。这是从具有编译器生成的复制构造函数和赋值运算符的类的简单继承。一切都很简单,无需任何技巧即可完成:)