为什么dereference运算符在C++中保留多态(后期绑定)?

Pen*_*ang 8 c++ polymorphism

众所周知,"只有在通过引用或指针进行调用时才能在运行时解析虚拟 ." 因此,当我发现取消引用运算符也保持动态绑定功能时,令我惊讶的是.

#include <iostream>
using namespace std;

struct B {
  virtual void say() { cout << "Hello B" << endl; }
};

struct D : B {
  void say() override { cout << "Hello D" << endl; }
};

int main() {
    D *ptr = new D();
    B *p = ptr;
    (*p).say();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出是

Hello D
Run Code Online (Sandbox Code Playgroud)

问题:编译器处理解除引用运算符*的是什么?

我以为它是在编译时完成的.因此,当编译器使用指针p时,它应该假定p指向类型B的对象.例如,下面的代码

D temp = (*p);
Run Code Online (Sandbox Code Playgroud)

抱怨

error: no viable conversion from 'B' to 'D'
Run Code Online (Sandbox Code Playgroud)

Lig*_*ica 8

从表面上看,这是一个有趣的问题,因为没有一元的重载*,解除引用导致左值B,而不是引用类型.然而,即使开始沿着这条推理线走下去也是一个红色的鲱鱼:表达式永远不会有引用类型,因为引用会立即被删除并确定值类别.从这个意义上讲,一元运算*符非常类似于返回引用的函数

实际上,答案是你的初始断言是不正确的:动态调度根本不依赖于引用或指针.它是引用和指针,可以防止切片,但是一旦你有一些表达式引用你的多态对象,任何旧的函数调用都可以.

还要考虑:

#include <iostream>

struct Base
{
   virtual void foo() { std::cout << "Base::foo()\n"; }
   void bar() { foo(); }
};

struct Derived : Base
{
   virtual void foo() { std::cout << "Derived::foo()\n"; }
};

int main()
{
   Derived d;
   d.bar();    // output: "Derived::foo()"
}
Run Code Online (Sandbox Code Playgroud)

(现场演示)

  • @ Cheersandhth.-Alf:是的,类型`T`的左值表达式不是引用类型.所以有什么问题? (4认同)
  • 嗯,我不确定我是否喜欢*动态调度根本不依赖于引用或指针*.vtable使用函数指针(它必须),并且对象需要有一个指向vtable的指针或存储vtable本身.切片甚至不必阻止动态调度,只是因为我没有弄错,C++构造函数不会复制该指针,而是单独为每个对象设置它(这是一件好事,因为*可能*缺少派生班级数据成员). (2认同)
  • @dyp:`(*p).foo()`执行动态调度,而`(*p)`既不是指针也不是引用类型,这正是OP所要求的现象,而且它显而易见!(在虚拟调度的_implementation_中是否使用指针语义既不在这里也不在那里,我假设.) (2认同)

dyp*_*dyp 5

derefencing/indirection运算符*本身并不执行任何操作.例如,当你只编写时,*p;如果p只是一个指针,编译器可能会忽略这一行.

它的*作用是什么改变了读写的语义:

int  i = 42;
int* p = &i;

*p = 0;
 p = 0;
Run Code Online (Sandbox Code Playgroud)

*p = 0方法写入对象p.请注意,在C++中,对象 存储区域.

同样的,

auto x =  p; // copies the address
auto y = *p; // copies the value
Run Code Online (Sandbox Code Playgroud)

这里,读取*p意味着读取对象的值p指向.

值类别*p仅确定C++语言允许对表单的表达式执行哪些操作*p.

引用实际上只是语法糖的指针.因此,试图*p通过使用引用来解释什么是循环推理.


让我们考虑稍微改变一下的类:

class Base
{
private:
    int b = 21;
public:
    virtual void say() { std::cout << "Hello B(" <<b<< ")\n"; }
};

class Derived : public Base
{
private:
    int d = 1729;
public:
    virtual void say() { std::cout << "Hello D(" <<d<< ")\n"; }
};


Derived d;
Derived *pd = &d;
Base* pb = pd;
Run Code Online (Sandbox Code Playgroud)

有点奇怪,但我认为允许的内存布局如下所示:

$$2d graphics mode$$

        +-Derived------------+
        |    +-Base---+----+ |
        | d  | vtable | b  | |
        |    +--------+----+ |
        +----^---------------+
        ^    | pb
        | pd


$$1d graphics mode$$

name    #    /../   |d       |vtable          |b       |
address #   /../    |0 1 2 3 |4 5 6 7 8 9 1011|12131415|16
                     ^        ^
                     | pd     | pb

pd == some address
pb == pd + 4 byte

当我们从转换Derived*Base*,编译器知道的偏移Base一个内部子对象Derived的对象,可以计算该子对象的地址值.

对于单个非虚拟继承,vtable指针存储在具有虚函数的最少派生类型中.它大致如本实现/模拟中所见,由派生类更改.

我们现在打电话

pb->say()
Run Code Online (Sandbox Code Playgroud)

在C++标准中定义为

(*pb).say()
Run Code Online (Sandbox Code Playgroud)

编译器根据pb(即Base*)的类型知道我们称之为虚函数.因此,(*pb).say()手段查找的条目say在虚函数表 的对象pb,并调用它.对象pb的一部分指向允许多态的内容.

另一方面,当我们复制

Base b = *pb;
Run Code Online (Sandbox Code Playgroud)

会发生什么是复制vtable指针.这将是危险的,因为Derived::say可能会尝试访问Derived::d.但是这个数据成员在Base我们当前正在创建的类型的对象中不可用(在复制文件中Base).