C++动态调度和后期绑定有什么区别

What is the difference between dynamic dispatch and late binding in C++?

本文关键字:什么 区别 绑定 动态调度 C++      更新时间:2023-10-16

我最近在维基百科上读到了动态调度,无法理解动态调度和C++后期绑定之间的区别。

何时使用每种机制?

来自维基百科的确切引用:

动态

调度不同于后期绑定(也称为动态绑定(。在选择操作的上下文中,绑定是指将名称与操作相关联的过程。调度是指在确定名称引用哪个操作后为操作选择实现。使用动态调度时,名称可以在编译时绑定到多态操作,但直到运行时才会选择实现(这就是动态调度在 C++ 中的工作方式(。但是,后期绑定确实意味着动态调度,因为在选择名称引用的操作之前,您无法选择要选择的多态操作的实现。

一个相当不错的答案实际上被纳入了关于 programmers.stackexchange.com 的晚期与早期绑定的问题中。

简而言之,后期绑定是指 eval 的对象端,动态调度是指功能端。在后期绑定中,变量的类型是运行时的变体。在动态调度中,正在执行的函数或子例程是变体。

在C++中,我们实际上没有后期绑定,因为类型是已知的(不一定是继承层次结构的末尾,但至少是正式的基类或接口(。但我们确实通过虚拟方法和多态性进行了动态调度。

我可以为后期绑定提供的最好示例是 Visual Basic 中的非类型化"对象"。运行时环境会为您完成所有后期绑定的繁重工作。

Dim obj
- initialize object then..
obj.DoSomething()

编译器实际上将为运行时引擎编写适当的执行上下文,以执行名为 DoSomething 的方法的命名查找,如果使用正确匹配的参数发现,则实际执行底层调用。实际上,有关对象类型的某些信息是已知的(它继承自IDispatch并支持GetIDsOfNames()等(。但就语言而言,变量的类型在编译时是完全未知的,并且在运行时到达执行点之前,它不知道DoSomething是否是一种方法,用于任何实际obj

我不会费心转储一个C++虚拟界面等,因为我相信你已经知道它们是什么样子的。我希望很明显,C++语言根本无法做到这一点。它是强类型。它可以(并且确实(通过多态虚拟方法功能进行动态调度。

在C++中,两者是相同的。

在C++中,有两种类型的绑定:

  • 静态绑定 — 在编译时完成。
  • 动态绑定 — 在运行时完成。
动态绑定,

因为它是在运行时完成的,因此也称为后期绑定,静态绑定有时也称为早期绑定

使用动态绑定,C++通过函数(或函数指针(支持运行时多态性,并使用静态绑定,解析所有其他函数调用。

后期绑定是在运行时按名称调用方法。 在 c++ 中,除了从 DLL 导入方法之外,您实际上没有此功能。
这方面的一个例子是:GetProcAddress((

通过动态调度,编译器有足够的信息来调用方法的正确实现。这通常通过创建虚拟表来完成。

链接本身解释了其中的区别:

动态

调度不同于后期绑定(也称为动态绑定(。在选择操作的上下文中,绑定是指将名称与操作相关联的过程。调度是指在确定名称引用哪个操作后为操作选择实现。

使用动态调度

时,名称可以在编译时绑定到多态操作,但直到运行时才会选择实现(这就是动态调度在 C++ 中的工作方式(。但是,后期绑定确实意味着动态调度,因为在选择名称引用的操作之前,您无法选择要选择的多态操作的实现。

但它们在C++可以通过虚拟函数和虚拟表进行动态调度,它们大多是相等的。

C++使用早期绑定,并提供动态和静态调度。默认的调度形式是静态的。要获取动态调度,您必须将方法声明为虚拟方法。

绑定是指将名称与操作相关联的过程。

这里最主要的是函数参数,这些参数决定了在运行时调用哪个函数

调度是指在确定名称引用哪个操作后为操作选择实现。

根据参数匹配调度控制

http://en.wikipedia.org/wiki/Dynamic_dispatch

希望这对你有帮助

让我给你一个差异的例子,因为它们不一样。 是的,动态调度允许您在通过超类引用对象时选择正确的方法,但这种魔力非常特定于该类层次结构,您必须在基类中执行一些声明才能使其工作(抽象方法填写 vtables,因为表中方法的索引不能在特定类型之间更改(。 因此,您可以通过通用的 Cat 指针调用 Tabby 和 Lion and Tiger 中的方法,甚至可以用狮子、老虎和虎斑填充猫数组。 它知道这些方法在编译时(静态/早期绑定(在对象的 vtable 中引用哪些索引,即使该方法是在运行时选择的(动态调度(。

现在,让我们实现一个包含狮子、老虎和熊的数组! ((天哪! 假设我们没有一个名为 Animal 的基类,在C++中,您将有大量的工作要做,因为编译器不会让您在没有公共基类的情况下进行任何动态调度。 vtables 的索引需要匹配,而这在未关联的类之间无法完成。 你需要有一个足够大的vtable来容纳系统中所有类的虚拟方法。 C++程序员很少认为这是一种限制,因为你已经被训练过,可以以某种方式思考类设计。 我不是说它更好或更坏。

使用后期绑定时,运行时无需公共基类即可解决此问题。 通常有一个哈希表系统用于查找类中的方法,调度程序中使用缓存系统。 在C++中,编译器知道所有类型。 在后期绑定语言中,对象本身知道它们的类型(它不是无类型,在大多数情况下,对象本身确切地知道它们是谁(。 这意味着如果我愿意,我可以拥有多种类型对象的数组(狮子、老虎和熊(。 而且,与不支持后期绑定的语言相比,您可以实现消息转发和原型设计(允许在不更改类的情况下更改每个对象的行为(以及各种其他事情,并且代码开销要少得多。

曾经在Android中编程并使用findViewById((吗? 你几乎总是最终转换结果以获得正确的类型,而强制转换基本上是对编译器撒谎,放弃了所有应该使静态语言优越的静态类型检查优点。 当然,你可以改用 findTextViewById((、findEditTextById(( 和其他一百万个,以便你的返回类型匹配,但这会把多态性抛出窗外;可以说是OOP的全部基础。 后期绑定语言可能会让您简单地按 ID 进行索引,并将其视为哈希表,而不关心索引或返回的类型。

这是另一个例子。 假设你有你的狮子类,它的默认行为是当你看到它时吃掉你。 在C++中,如果你想拥有一只"训练有素"的狮子,你需要创建一个新的子类。 原型设计可以让您简单地更改需要更改的特定狮子的一两种方法。 它的类和类型不会改变。 C++做不到。 这很重要,因为当你有一个新的"非洲斑点狮"继承自狮子时,你也可以训练它。 原型不会更改类结构,因此可以对其进行扩展。 这通常是这些语言处理通常需要多重继承的问题的方式,或者多重继承可能是您处理缺乏原型的方式。

仅供参考,Objective-C 是添加了 SmallTalk 消息传递的 C,SmallTalk 是原始的 OOP,两者都是具有上述所有功能以及更多功能的后期绑定。 从微观层面的角度来看,后期绑定语言可能稍微慢一些,但通常可以允许代码以宏观层面更有效的方式构建,这一切都归结为偏好。

鉴于维基百科冗长的定义,我很想将动态调度归类为C++的后期绑定

struct Base {
    virtual void foo(); // Dynamic dispatch according to Wikipedia definition
    void bar();         // Static dispatch according to Wikipedia definition
};

相反,对于维基百科来说,延迟绑定似乎意味着指向成员发送C++

(this->*mptr)();

其中,选择正在调用的操作(而不仅仅是哪个实现(是在运行时完成的。

然而,在C++文献中,late binding通常用于维基百科所谓的动态调度。

动态调度是当您在C++中使用 virtual 关键字时发生的情况。所以例如:

struct Base
{
    virtual int method1() { return 1; }
    virtual int method2() { return 2; } // not overridden
};
struct Derived : public Base
{
    virtual int method1() { return 3; }
}
int main()
{
    Base* b = new Derived;
    std::cout << b->method1() << std::endl;
}

将打印3,因为该方法已被动态调度。C++标准非常小心,没有指定幕后究竟是如何发生的,但阳光下的每个编译器都以相同的方式做到这一点。它们为每个多态类型(称为虚拟表或 vtable(创建一个函数指针表,当您调用虚拟方法时,将从 vtable 中查找"real"方法,并调用该版本。因此,您可以对类似此伪代码的内容进行成像:

struct BaseVTable
{
    int (*_method1) () = &Base::method1; // real function address
    int (*_method2) () = &Base::method2;
};
struct DerivedVTable
{  
    int (*method1) () = &Derived::method1; //overriden
    int (*method2) () = &Base::method2; // not overridden
};

这样,编译器可以确保在编译时存在具有特定签名的方法。但是,在运行时,调用实际上可能通过 vtable 调度到不同的函数。由于额外的间接步骤,对虚拟函数的调用比非虚拟调用慢一点。

<小时 />

另一方面,我对术语后期绑定的理解是,函数指针在运行时按名称、哈希表或类似内容查找。这是在Python,JavaScript和(如果没记错的话(Objective-C中完成工作的方式。这使得在运行时向类添加新方法成为可能,这不能直接在C++中完成。这对于实现 mixins 之类的东西特别有用。但是,缺点是运行时查找通常比C++中的虚拟调用慢得多,并且编译器无法对新添加的方法执行任何编译时类型检查。

这个问题可能会对你有所帮助。

动态派单一般指多次分派。

请考虑以下示例。我希望它能帮助你。

    class Base2;
    class Derived2; //Derived2 class is child of Base2
class Base1 {
    public:
        virtual void function1 (Base2 *);
        virtual void function1 (Derived2 *);
}
class Derived1: public Base1 {
    public:
    //override.
    virtual void function1(Base2 *);
    virtual void function1(Derived2 *);
};

考虑下面的情况。

Derived1 * d = new Derived1;
Base2 * b = new Derived2;
//Now which function1 will be called.
d->function1(b);

它会称function1采取Base2*而不是Derived2*.这是由于缺少动态多重调度。

后期绑定是实现动态单次调度的机制之一。

我想意思是当你有两个类B时,C继承了同一个父亲类A。 所以,父亲的指针(A型(可以容纳每个儿子的类型。编译器无法知道该类型在特定时间内在指针中保存的内容,因为它可以在程序运行期间更改。

有特殊的函数来确定特定对象在特定时间内的类型,例如Java中的instanceof,或C ++中的if(typeid(b) == typeid(A))...

在C++中,dynamic dispatchlate binding是相同的。基本上,单个对象的值决定了在运行时调用的代码段。在C++和java等语言中,动态调度更具体地说是动态单一调度,其工作原理如上所述。在这种情况下,由于绑定发生在运行时,因此也称为 late binding 。像smalltalk这样的语言允许动态多次调度,其中运行时方法在运行时根据多个对象的标识或值选择。

在C++中,我们实际上没有后期绑定,因为类型信息是已知的。因此,在C++或 Java 上下文中,动态调度和后期绑定是相同的。实际/完全后期绑定,我认为是在像python这样的语言中,它是一种基于方法的查找而不是基于类型的查找。