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

时间:2014-03-25 02:20:54

标签: c++ oop multiple-inheritance language-lawyer member-function-pointers

最初,有C.
C有结构,表达式和函数来打包它们。这很好。
但是C也有goto和switch case掉线和随后使用的语法,所以可能不那么好。

它也有指针,导致混淆和指针算术咬牙切齿! 但是它也有函数指针,允许运行时调度,然后很多欢乐 现在,数据可以指示代码,也可以指示数据的代码,两者都是头等(或接近) 对于任何可以指出的东西,可以用同一个指针指出:圣洁的空虚*。

所有人的荣耀都是平等的。

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

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

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

然而,任何可以指出的东西,都可以指向神圣的虚空*。

使用虚拟,我们失去了简单,并且漫游了很长时间 想知道,如何处理钻石,或者不是椭圆形的圆圈 但即使我们抛弃了C的旧租户,每一条指令都简化为简单的asm,
我们接受了,有些事情应该看起来很简单(即使它们很复杂)。

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

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

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

因此,必须在每次虚拟呼叫时进行指针调整 因为Base类实际上可能是伪装的MI派生类,需要进行调整 因此,对于使用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();
}

如此处所示,在调用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(); }
}

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

最重要的是,任何可以指出的东西,都可以通过圣洁的空虚指向* 所有成员函数指针都需要是一个普通的函数指针,它带有一个特殊的第一个参数 事情本来可以很好。

如果我们只生活在这样一个梦想的世界里。

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

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

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

事实上,如The Fastest Possible Delegates所述,当前的解决方案实际上减慢了每个虚拟调用的速度;它通过胖指针强制额外检查和额外的内存使用,即使对于单继承,也必须存储额外的数据(或者冒失去它的风险,因为MSVC成员函数指针可以)。如果您使用的话,这显然不在“付款”中,如果您不这样做的话。 C ++哲学!

因此,重申一下,为什么成员函数指针不同于"松散"函数指针? 是否有任何逻辑上的原因,为什么它们不仅仅是具有特殊调用约定的函数指针,或者是"这个"?

的额外参数

1 个答案:

答案 0 :(得分:6)

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

希望这有帮助!