为什么成员函数指针与C++中的普通函数指针不同?

Ali*_*ice -7 c++ oop member-function-pointers multiple-inheritance language-lawyer

在开始时,有C.
和C有结构,表达和功能来打包它们.这很好.
但是C也有goto和switch case掉线和随后使用的语法,所以可能不那么好.

它也有指针,从别名和指针算术中引起大量咬牙切齿!
但它也有函数指针,允许运行时调度,并随后欢喜.
现在,数据可以指示代码,以及指示数据的代码,并且都是头等(或接近).
对于任何可以指出的东西,可以用同一个指针指出:圣洁的空虚*.

它的荣耀都是平等的.

然后C++来了,并将数据和代码绑定到Object中.
而且,这只是语法糖,因为函数和方法没有那么不同,
(无论Sun或Oracle可能告诉你什么).

Obj-> Foo(int val)与Foo(Obj*this,int val)相同(大约),它们
在神圣的空隙*下仍然相等.

然后,继承,冲突,因为外部Derived类可能会添加到内部Base.
但是,在这些简单的时代,我们找到了一个解决方案:在Derived之前放入每个Base.
然后,使用相同的指针,我们可以指向孩子和父亲.

而且,任何可以指出的东西,都可以指向圣洁的空虚*.

随着虚拟,我们失去了我们的简单,并徘徊了很长时间.
想知道,如何处理钻石,或者不是椭圆形的圆圈.
但是,即使我们抛弃了C的旧租户,每个指令都简化为简单的asm,
我们也接受了,有些东西应该看起来很简单(即使它们很复杂).

因此,我们查看了出现并认为"足够好"的秘密VTables.
在我们引入隐藏数据的同时,我们降低了复杂性.
现在,通过虚拟机进行的任何呼叫都通过VTable重定向.
由于一个类的所有子对象都可以通过单个指针指向,这就足够了.

但即使调度方法发生了变化,我们仍然可以指出所有事情,圣洁无效*.

但后来有人提出,目前很多人都认为这是一个严重的错误:多重继承.
并且一个指针不再足够!两位基础父亲如何才能在一开始?
那么,VTable就不再足够了,因为我们怎么知道要指向哪个子对象!
而现在,钻石的问题比以前更糟糕,没有明显的解决方案,而我们之前的钻石需要现有的代码来应对未来的前景!

因此,必须在每次虚拟呼叫时进行指针调整.
因为Base类可能真的是伪装的MI Derived类,需要进行调整.
因此,对于使用MI的Choice Few的支持,我们都付出了代价.

突然之间,圣洁的空虚*再也无法储存那些简单的糖.
处理这种复杂性的原因是可怕的成员函数指针.

野兽需要它自己的语法,因为其他人都不够.
而且这种语法很少使用,它的优先级如此之低,以至于每次使用都需要parans.
虽然在神圣的标准中,邪恶的议会决定允许这些可怜的东西被投射,
但是当从类型转换为类型时,不调用而不调用行为最不明确!

然而,在语法和贪婪方面颓废,他们是胖子,无法适应无效*.
作为了解要指向的对象的唯一方法是进行调整,
深入嵌入指针,并使用每个VTable查找进行检查.

但兄弟们,这不是必须的.
这种实施的复杂性来自于一种最特殊的决策.

class Base1
{
public:
    virtual void foo();
};

class Base2
{
public:
    virtual void bar();
};

class Derived: public Base1, public Base2
{
public:
    void unrelated();
}
Run Code Online (Sandbox Code Playgroud)

如此处所示,在调用foo()或bar()时必须调整Derived*; 它不能同时指向Base1和Base2,就像简单单继承的情况一样.实际上,从基类调用时,确实无法正确预测需要多少偏移,这就是为什么大多数都有某种机制将其添加到vtable中.

然而:

class Derived: public Base1, public Base2
{
public:
    void unrelated();

    virtual void foo() { Base1::foo(); }
    virtual void bar() { Base2::bar(); }
}
Run Code Online (Sandbox Code Playgroud)

解决问题,不需要改变原始对象模型!
由于每个方法现在都存在,它可以正确地添加到vtable中,并且在被调用时,确切地知道调整指针的数量,允许调用继续进行而不会有任何混乱!
而且现在,在构建成员函数指针以及在强制转换时调用它都是明确定义的!

最重要的是,任何可以指出的东西,都可以通过圣洁的空洞来指出.
所有成员函数指针都需要是一个普通函数指针,它采用一个特殊的第一个参数.
而且,事情会非常好.

只要我们生活在这样一个梦想世界.

对我们来说不幸的是,我们使用胖成员函数指针,必须调整每个调用的vtable,以及无法正确创建委托或许多其他有用模式的成员函数指针.在MSVC中,当你施放它们时它们会改变大小!

问题是如此之大,以至于std :: function可以在大多数实现中动态分配内存,因为这里列出了各种问题.正如我已经详述的那样,使用thunk用于unoverriden方法可以非常方便地解决这个问题,并且这个成本是一些无法使用的隐藏方法,以及一些vtable更改,以及可能(但很小)的速度降低虚拟调度,在函数未被覆盖且无法完美内联的情况下.

对于速度和空间的这种微不足道的增加,我们在成员函数指针中创建了一个怪物,并且影响了六种其他语言,尽管容易解决问题,使用了阉割成员函数指针,但是不使用多重继承,并使我们的代表较慢.

事实上,正如最快可能的代表所述,当前的解决方案实际上减慢了每个虚拟调用的速度; 它通过胖指针强制额外检查和额外的内存使用,即使对于单继承,也必须存储额外的数据(或者冒失去它的风险,因为MSVC成员函数指针可以).这显然不是在"付费,如果你使用,而不是你不使用"的C++哲学!

那么,重申一下,为什么成员函数指针与"松散"函数指针不同?是否有任何逻辑上的原因,它们不仅仅是具有特殊调用约定的函数指针,还是具有"this"的额外参数?

tem*_*def 6

C++有一个"不为你不使用的东西付费"的规则,这意味着不应该减慢正常操作以支付程序员不使用的其他语言功能.虽然你绝对可以使成员函数指针与普通函数指针相同,但是动态调度中的因子,不同的vtable偏移,不同的基础对象偏移和thunks等的额外开销会给正常的函数指针带来额外的开销,这要么是由于存储此信息所需的额外内存(更大的大小)或执行调度所需的额外逻辑(额外的时间和生成的代码).因此,将函数指针和成员函数指针拆分为具有单独实现的单独类型是有意义的.

希望这可以帮助!

  • @Alice C++哲学是"如果你不想为'std :: function`付钱,你就不必使用它.你可以随时只使用原始函数指针或成员函数指针." 也许我错过了你的问题的重点 - 我是否正确你的问题是"为什么C++将函数指针与成员函数指针分开?" 另外,你能详细说明我是如何"绝对错误的",C++如何"削弱"成员函数指针,以及"如何"存在更多的语法问题?" (3认同)
  • 此外,还有一个您遗漏的最后一个细节 - C++编译器在编译单个类时不一定具有完整的继承图.由于C++支持一次通过编译,编译器只要看到类就需要生成最好的代码,这意味着它可能不得不悲观地假设会有多个继承或其他奇怪的动态调度.在这些条件下,到目前为止工作的解决方案是我们实际使用的解决方案. (2认同)