【问题标题】:What is the difference between dynamic dispatch and late binding in C++?C ++中的动态调度和后期绑定有什么区别?
【发布时间】:2013-11-25 08:15:52
【问题描述】:

我最近在Wikipedia 上阅读了有关动态调度的信息,但无法理解 C++ 中动态调度和后期绑定之间的区别。

什么时候使用每一种机制?

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

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

【问题讨论】:

  • 这是一个很好的问题,如果你提到你读过的链接会更好。

标签: c++ oop


【解决方案1】:

一个相当不错的答案实际上已包含在programmers.stackexchange.com 上关于晚期与早期绑定的问题中。

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

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

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

Dim obj

- initialize object then..
obj.DoSomething()

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

我不会费心转储 C++ 虚拟接口等,因为我相信您已经知道它们的样子。我希望很明显,C++ 语言根本无法做到这一点。它是强类型的。它可以(并且显然)通过多态虚拟方法特性进行动态调度。

【讨论】:

  • 我已经尽力支持这个答案,这实际上解释了差异。太糟糕了,OP 被 131k 代表蒙蔽了双眼 - 并选择了最糟糕的答案......
  • @IInspectable 一切都很好。我通常会在一周左右后放弃我发布的任何没有投票的答案,因为如果没有人认为它们有帮助,我不希望他们把“接受”的答案弄得乱七八糟。但我很高兴有人发现这里的差异解释是值得的,所以我现在可能会保留它。感谢您的支持。
  • 动态调度只是后期绑定的一个特定实例,其中方法选择器是名称,目标方法是要解析的方法。动态调度基本上是一个部分评估的后期绑定。
  • @ZanLynx 他们是不同的。在 VB 中,langauge 除了你(程序)想要触发一个名为DoSomthing 的方法之外,对那个对象一无所知(或者它甚至是一个有效的对象)。不要将 languageruntime 混淆。我试图说明这一点,但老实说,我自己对描述只是勉强满意。在 C++ 方面,you 必须自己通过 IDispatch 编写所有动态,即使这样,语言 也知道对象的某些内容(它支持 IDispatch等)。
  • 我是否正确假设 Java 仅具有动态调度,因为 Object 的类在某种意义上总是已知的,并且我引用(不一定是继承层次结构的结尾,而是在至少一个正式的基类或接口)。如果不是,多态的例子是什么?
【解决方案2】:

link 本身解释了差异:

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

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

但它们在 C++ 中基本相同,您可以通过虚函数和 vtable 进行动态调度。

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

【讨论】:

    【解决方案3】:

    后期绑定是在运行时按名称调用方法。 除了从 DLL 导入方法外,您在 C++ 中并没有这个功能。
    例如:GetProcAddress()

    使用动态分派,编译器有足够的信息来调用方法的正确实现。这通常通过创建 virtual table 来完成。

    【讨论】:

    • 动态调度,顾名思义,意味着它发生在运行时。这是因为编译器没有足够的信息。
    【解决方案4】:

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

    在 C++ 中,有两种绑定:

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

    动态绑定,因为它是在运行时完成的,所以也被称为后期绑定,而静态绑定有时被称为早期绑定

    使用动态绑定,C++ 通过虚拟函数(或函数指针)支持运行时多态,使用静态绑定,所有其他 函数调用已解决。

    【讨论】:

      【解决方案5】:

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

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

      分派是指在您确定名称所指的操作之后为操作选择实现。

      根据参数匹配调度控制

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

      希望对你有帮助

      【讨论】:

        【解决方案6】:

        让我举个例子来说明差异,因为它们并不相同。是的,当您通过超类引用对象时,动态分派可以让您选择正确的方法,但是这种魔法非常特定于该类层次结构,您必须在基类中进行一些声明才能使其工作(抽象方法填写虚表,因为表中方法的索引不能在特定类型之间更改)。因此,您可以通过通用 Cat 指针调用 Tabby 和 Lion 和 Tiger 中的方法,甚至可以将 Cats 数组填充为 Lions、Tigers 和 Tabbys。它知道那些方法在编译时(静态/早期绑定)在对象的 vtable 中引用的索引,即使在运行时选择了方法(动态调度) )。

        现在,让我们实现一个包含 Lions、Tigers 和 Bears 的数组! ((天啊!))。假设我们没有名为 Animal 的基类,在 C++ 中,您将有大量工作要做,因为编译器不会让您在没有公共基类的情况下进行任何动态调度。 vtables 的索引需要匹配,而这不能在不相关的类之间完成。您需要有一个足够大的 vtable 来保存系统中所有类的虚拟方法。 C++ 程序员很少将此视为一种限制,因为您已经受过训练,可以以某种方式思考类设计。我并不是说它的好坏。

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

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

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

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

        【讨论】:

          【解决方案7】:

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

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

          对于 Wikipedia 而言,后期绑定似乎意味着 C++ 的指向成员的指针调度

          (this->*mptr)();
          

          在运行时选择什么是被调用的操作(而不仅仅是哪个实现)。

          然而,在 C++ 文献中,late binding 通常用于 Wikipedia 所称的动态调度。

          【讨论】:

            【解决方案8】:

            当您在 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++ 标准非常谨慎指定这在幕后究竟是如何发生的,但是太阳下的每个编译器都以相同的方式进行。它们为每个多态类型创建一个函数指针表(称为 virtual tablevtable),当您调用虚拟方法时,会查找“真实”方法从 vtable 中,并调用该版本。所以你可以像这样的伪代码成像:

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

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


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

            【讨论】:

              【解决方案9】:

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

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

              考虑下面的例子。希望对你有帮助。

                  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);
              

              它将调用function1Base2* 而不是Derived2*。这是由于缺乏动态多分派。

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

              【讨论】:

                【解决方案10】:

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

                有一些特殊的函数可以确定某个对象在某个时间是什么类型。就像java中的instanceof,或者c++中的if(typeid(b) == typeid(A))...

                【讨论】:

                  【解决方案11】:

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

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

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2010-12-10
                    • 1970-01-01
                    • 2017-05-22
                    • 1970-01-01
                    • 1970-01-01
                    • 2012-06-06
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多