memcpy派生类到基类,为什么还调用基类函数

时间:2016-12-12 04:23:46

标签: c++ undefined-behavior lifetime dynamictype vptr

我正在阅读在C ++对象模型中。在第1.3节

  

那么,那么,为什么呢,给定

Bear b; 
ZooAnimal za = b; 

// ZooAnimal::rotate() invoked 
za.rotate(); 
  

调用的rotate()实例是ZooAnimal实例,而不是Bear的实例?此外,如果成员初始化将一个对象的值复制到另一个对象,为什么za的vptr不能解决Bear的虚拟表?

     

第二个问题的答案是编译器在一个类对象的初始化和赋值中干扰另一个。编译器必须确保如果对象包含一个或多个vptrs,源对象不会初始化或更改这些vptr值。

所以我在下面写了测试代码:

#include <stdio.h>
class Base{
public:
    virtual void vfunc() { puts("Base::vfunc()"); }
};
class Derived: public Base
{
public:
    virtual void vfunc() { puts("Derived::vfunc()"); }
};
#include <string.h>

int main()
{
    Derived d;
    Base b_assign = d;
    Base b_memcpy;
    memcpy(&b_memcpy, &d, sizeof(Base));

    b_assign.vfunc();
    b_memcpy.vfunc();

    printf("sizeof Base : %d\n", sizeof(Base));

    Base &b_ref = d;
    b_ref.vfunc();

    printf("b_assign: %x; b_memcpy: %x; b_ref: %x\n", 
        *(int *)&b_assign,
        *(int *)&b_memcpy,
        *(int *)&b_ref);
    return 0;
}

result

Base::vfunc()
Base::vfunc()
sizeof Base : 4
Derived::vfunc()
b_assign: 80487b4; b_memcpy: 8048780; b_ref: 8048780

我的问题是为什么b_memcpy仍然调用Base :: vfunc()

4 个答案:

答案 0 :(得分:2)

您正在做的事情在C ++语言中是非法的,这意味着您的b_memcpy对象的行为未定义。后者意味着任何行为都是“正确的”,你的期望是完全没有根据的。尝试分析未定义的行为并没有多大意义 - 它不应该遵循任何逻辑。

实际上,使用memcpy的操作很可能实际上将Derived的虚拟表指针复制到b_memcpy对象。您使用b_ref进行的实验证实了这一点。但是,当通过直接对象调用虚方法时(与b_memcpy.vfunc()调用的情况一样),大多数实现都会优化对虚拟表的访问并执行 direct 非虚拟)调用目标函数。语言的正式规则表明,任何法律行为都不能使b_memcpy.vfunc()调用分派给Base::vfunc()以外的任何内容,这就是为什么编译器可以通过直接调用{{1}来安全地替换此调用的原因}。这就是为什么任何虚拟表操作通常都不会对Base::vfunc()调用产生影响。

答案 1 :(得分:1)

您调用的行为未定义,因为标准表明它未定义,并且您的编译器利用了这一事实。让我们看一下g ++的具体例子。它为禁用优化的行b_memcpy.vfunc();生成的程序集如下所示:

lea     rax, [rbp-48]
mov     rdi, rax
call    Base::vfunc()

如你所见,vtable甚至没有被引用。由于编译器知道b_memcpy的静态类型,因此没有理由以多态方式调度该方法调用。 b_memcpy不能是Base个对象,因此它只会生成对Base::vfunc()的调用,就像调用任何其他方法一样。

进一步,让我们添加一个这样的函数:

void callVfunc(Base& b)
{
  b.vfunc();
}

现在,如果我们致电callVfunc(b_memcpy);,我们可以看到不同的结果。在这里,我们得到一个不同的结果取决于我编译代码的优化级别。在-O0和-O1上调用Derived::vfunc()并打印-O2和-O3 Base::vfunc()。同样,由于标准表明程序的行为未定义,编译器不会努力产生可预测的结果,而只是依赖于语言所做的假设。由于编译器知道b_memcpyBase对象,因此当优化级别允许时,它可以简单地内联对puts("Base::vfunc()");的调用。

答案 2 :(得分:0)

你不能做

memcpy(&b_memcpy, &d, sizeof(Base));

- 它的未定义行为,因为b_memcpyd不是普通旧数据&#34;对象(因为它们具有虚拟成员函数)。

如果你写了:

b_memcpy = d;

然后按预期打印Base::vfunc()

答案 3 :(得分:0)

vptr的任何使用都超出了标准范围

当然,这里使用memcpy有UB

答案指出任何使用memcpy或非POD的其他字节操作,即任何具有vptr的对象,都有未定义的行为,在技术上严格正确但不回答问题。 这个问题是基于vptr(vtable指针)的存在,它甚至不是由标准强制执行的:当然答案将涉及标准之外的事实,结果法案不能得到保证按标准!

标准文本与vptr

无关

问题不在于不允许操纵vptr;标准允许操纵标准文本中甚至没有描述的任何内容的概念是荒谬的。当然,不存在更改vptr的标准方法,这是不重要的。

vptr编码多态对象的类型

这里的问题不是标准所说的关于vptr的问题,问题是vptr代表什么,标准说的是什么:vptr表示对象的动态类型。只要操作的结果取决于动态类型,编译器就会生成使用vptr的代码。

[关于MI的说明:我说&#34;&#34; vptr(就好像只有一个vptr),但是当涉及MI(多重继承)时,对象可以有多个vptr,每个vptr表示被视为特定多态基类类型的完整对象。 (多态类是具有至少一个虚函数的类。)]

[关于虚拟基础的注意事项:我只提到vptr,但是一些编译器插入其他指针来表示动态类型的各个方面,比如虚拟基础子对象的位置,而其他一些编译器则使用vptr来实现此目的。对于这些其他内部指针,vptr的真实情况也是如此。]

所以 vptr的特定值对应于动态类型:这是大多数派生对象的类型。

对象在其生命周期中的动态类型的变化

在构造期间,动态类型会发生变化,这就是构造函数内部的虚函数调用可能会令人惊讶的原因#34;有人说在构造过程中调用虚函数的规则是特殊的,但它们绝对不是:最终的覆盖被调用; override是与构造的最派生对象相对应的类,在构造函数C::C(arg-list)中,它始终是类C的类型。

在销毁期间,动态类型以相反的顺序更改。从析构函数内部调用虚函数遵循相同的规则。

未定义某些内容时的含义

您可以执行标准中未批准的低级别操作。 在C ++标准中没有明确定义行为并不意味着它没有在别处描述。仅仅因为明确描述了操作的结果,在C ++标准中具有UB(未定义的行为)并不意味着您的实现无法定义它。

您还可以使用您对编译器工作方式的了解:如果使用严格的单独编译,即编译器无法从单独编译的代码中获取任何信息,则每个单独编译的函数都是&#34;黑盒子&# 34 ;.你可以使用这个事实:编译器必须假设单独编译的函数可以做的任何事情都可以完成。即使在给定函数内部,您也可以使用asm指令来获得相同的效果:没有约束的asm指令可以执行C ++中合法的任何操作。效果是&#34;忘记你从代码分析中得到的知识&#34;指令。

该标准描述了什么可以改变动态类型,除了构造/破坏之外什么都不允许改变它,所以只有&#34;外部&#34; (blackbox)函数是否允许执行构造/销毁可以改变动态类型。

不允许在现有对象上调用构造函数,除非使用完全相同的类型重构它(并且有限制),请参阅[basic.life]/8

  

如果在对象的生命周期结束之后和存储之前   对象占用的是重用或释放的,一个新的对象是   在原始对象占用的存储位置创建,a   指向原始对象的指针,引用的引用   到原始对象,或原始对象的名称   自动引用新对象,一旦生命周期   新对象已启动,可用于操作新对象,如果:

     

(8.1)新对象的存储正好覆盖存储   原始对象占用的位置,

     

(8.2)新对象与原始对象的类型相同   (忽略顶级cv限定符)和

     

(8.3)原始对象的类型不是const限定的,如果   类类型,不包含任何类型的非静态数据成员   是const限定的或引用类型,

     

(8.4)原始对象是派生最多的对象([intro.object])   T类型和新对象是类型T的最派生对象(即   是的,它们不是基类子对象。)

这意味着您可以调用构造函数(使用placement new)并且仍然使用用于指定对象的相同表达式(其名称,指向它的指针等)的唯一情况是动态类型将不要改变,所以vptr仍然是一样的。

换句话说,如果你想用低级技巧覆盖vptr,你可以;但只有你写相同的值

换句话说,不要试图破解vptr。