1

I am reading Inside the C++ Object Model. In section 1.3

So, then, why is it that, given

Bear b; 
ZooAnimal za = b; 

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

the instance of rotate() invoked is the ZooAnimal instance and not that of Bear? Moreover, if memberwise initialization copies the values of one object to another, why is za's vptr not addressing Bear's virtual table?

The answer to the second question is that the compiler intercedes in the initialization and assignment of one class object with another. The compiler must ensure that if an object contains one or more vptrs, those vptr values are not initialized or changed by the source object .

So I wrote the test code below:

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

The result

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

My question is why b_memcpy still called Base::vfunc()

4

4 回答 4

2

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

在实践中,您的操作很可能memcpy确实将Derived的虚拟表指针复制到b_memcpy对象。你的实验b_ref证实了这一点。但是,当通过直接对象调用虚拟方法时(如b_memcpy.vfunc()调用的情况),大多数实现优化了对虚拟表的访问并执行对目标函数的直接非虚拟)调用。语言的正式规则规定,任何法律行为都不能b_memcpy.vfunc()调用 dispatch 以外的任何东西Base::vfunc(),这就是为什么编译器可以安全地用直接调用替换这个调用的原因Base::vfunc()。这就是为什么任何虚拟表操作通常不会对b_memcpy.vfunc()调用产生影响。

于 2016-12-12T04:29:59.190 回答
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 和 -O1Derived::vfunc()上调用,在 -O2 和 -O3Base::vfunc()上打印。同样,由于标准规定程序的行为是未定义的,编译器不会努力产生可预测的结果,而只是依赖于语言所做的假设。由于编译器知道b_memcpy是一个Base对象,它可以简单地将调用内联到puts("Base::vfunc()");优化级别允许的时间。

于 2016-12-12T04:54:35.357 回答
0

你不被允许做

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

- 这是未定义的行为,因为b_memcpy并且d不是“普通旧数据”对象(因为它们具有虚拟成员函数)。

如果你写:

b_memcpy = d;

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

于 2016-12-12T04:31:08.857 回答
0

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

当然,memcpy这里的使用有 UB

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

标准文本与 vptr 无关

问题不在于不允许操纵 vptr;标准允许操纵甚至没有在标准文本中描述的任何东西的想法是荒谬的。当然,不存在更改 vptr 的标准方法,这是题外话。

vptr 编码多态对象的类型

这里的问题不是标准对 vptr 的规定,问题是 vptr 代表什么,以及标准对此有何规定:vptr 代表对象的动态类型。每当操作的结果取决于动态类型时,编译器将生成代码以使用 vptr。

[关于 MI 的注意事项:我说的是“那个”vptr(好像只有一个 vptr),但是当涉及到 MI(多重继承)时,对象可以有多个 vptr,每个代表被视为特定多态基类的完整对象类型。(多态类是具有至少一个虚函数的类。)]

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

因此vptr 的特定值对应于动态类型:即大多数派生对象的类型。

对象生命周期内动态类型的变化

在构造过程中,动态类型会发生变化,这就是为什么从构造函数内部调用虚函数可能会“令人惊讶”。有人说构造时调用虚函数的规则很特殊,但绝对不是:调用final overrider;该 override 是与已构造的最派生对象相对应的类之一,并且在构造函数中C::C(arg-list),它始终是该类的类型C

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

未定义某事意味着什么

您可以进行标准中未批准的低级别操作。C++ 标准中未明确定义行为并不意味着其他地方未对其进行描述。仅仅因为在 C++ 标准中明确描述了操作的结果具有 UB(未定义的行为)并不意味着您的实现不能定义它。

您还可以使用您对编译器工作方式的了解:如果使用严格的单独编译,即编译器无法从单独编译的代码中获取任何信息,则每个单独编译的函数都是一个“黑匣子”。您可以使用这个事实:编译器将不得不假设单独编译的函数可以做的任何事情都会完成。即使在给定的函数内部,您也可以使用asm指令来获得相同的效果:asm没有约束的指令可以做任何在 C++ 中合法的事情。其效果是“忘记你从那时的代码分析中所知道的”指令。

该标准描述了可以更改动态类型的内容,并且除了构造/销毁之外不允许更改它,因此只有“外部”(黑盒)函数才被允许执行构造/销毁可以更改动态类型。

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

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原始对象占用的存储位置创建一个新对象,一个指向原始对象的指针,一个指向原始对象的引用引用原始对象,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

(8.1) 新对象的存储恰好覆盖了原始对象占用的存储位置,并且

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

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

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

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

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

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

于 2017-01-28T06:15:16.830 回答