为什么会执行错误的函数?

6

今天我遇到了一个令人困惑的情况,希望有人能够解释一下。

我有一个包含4个类的C++程序:

  • 一个Base类,只作为一个公共接口,
  • 一个Enroll类,它是Base的子类,并具有一个纯虚拟的enroll()方法,
  • 一个Verify类,也是Base的子类,并具有一个纯虚拟的verify()方法,
  • 一个Both类,它同时是EnrollVerify的子类,并提供了enroll()verify()的实现。

如下所示:

 class Base {
    public:
       Base () { }
       virtual ~Base () { }
 };

 class Enroll : public virtual Base {
    public:
       virtual ~Enroll () { }
       virtual void enroll () = 0;
 };

 class Verify : public virtual Base {
    public:
       virtual ~Verify () { }
       virtual void verify () = 0;
 };

 class Both : public Enroll, public Verify {
    public:
       virtual ~Both () { }

       virtual void enroll () { printf ("Enrolling.\n"); }
       virtual void verify () { printf ("Verifying.\n"); }
 };

Both的实例是在一个非成员函数中实例化的,它只创建一个新的Both并返回指针:

Both* createInstanceOfBoth () {
   return new Both();
}

最后,有一个Registry类,基本上只是作为Enroll/Verify工厂的作用。它使用一对函数指针来调用createInstanceOfBoth()函数,以提供EnrollVerify的实例:

typedef Enroll* (*EnrollGenerator) ();
typedef Verify* (*VerifyGenerator) ();

class Registry {
   public:
      Registry () {
          enrollGenerator = (EnrollGenerator)&createInstanceOfBoth;
          verifyGenerator = (VerifyGenerator)&createInstanceOfBoth;              
      }

      Enroll* getEnroll () { return enrollGenerator (); }
      Verify* getVerify () { return verifyGenerator (); }

      EnrollGenerator enrollGenerator;
      VerifyGenerator verifyGenerator;
};

以下是翻译的结果:

这里出现了问题。当我在Registry对象上调用getEnroll()并在返回的对象上调用enroll()时,我看到了正确的预期输出:Enrolling. 但是,当我调用getVerify()并在返回的对象上调用verify()时,enroll()方法会再次执行

代码:

int main () {
   Registry registry;
   Enroll *enroller;
   Verify *verifier;

   enroller = registry.getEnroll ();
   verifier = registry.getVerify ();
   enroller->enroll ();
   verifier->verify ();
   return 0;
}

输出:

Enrolling.
Enrolling.

我注意到,如果我在声明Both类时改变EnrollVerify的顺序(class Both : public Verify, public Enroll {...}),相反的效果将会发生:

Verifying.
Verifying.

我发现一个解决方法,不再使用单一的createInstanceOfBoth()函数创建我的EnrollVerify对象,而是使用两个不同命名的函数,它们具有相同的主体但是返回类型不同:

Enroll* createInstanceOfEnroll () {
   return new Both();
}
Verify* createInstanceOfVerify () {
   return new Both();
}

然后,在Registry类中,我创建指向这些函数的函数指针:
Registry () {
    enrollGenerator = &createInstanceOfEnroll;
    verifyGenerator = &createInstanceOfVerify;        
}

当我现在运行程序时,我得到了预期的输出:
Enrolling.
Verifying.

我的问题是:为什么第一种方法不起作用?我怀疑这与将createInstanceOfBoth()转换为具有不同返回类型的函数指针有关,但我不完全理解正在发生的事情。目前使用我的解决方法没问题,但我很好奇:是否有一种方法可以只使用单个createInstanceOfBoth()函数而不必使用两个具有不同返回类型但相同的函数来完成此操作?
为了节省时间和麻烦,我已将此代码发布到https://gist.github.com/833304以供下载。

我希望这只是一种学术上的好奇心... - AJG85
长篇问题,但排版清晰易读……做得好。 - Tony Delroy
@AJG85:没错。在探索动态实例化Base子类的方法时,我偶然遇到了这种情况,尽管生产代码不会涉及任何类似于这个例子的东西,但它仍然让我感到困惑,直到我理解了发生了什么才能放下并继续前进。 - RTBarnard
嗯,这是一个有趣的怪癖。听起来可能是你可以使用抽象工厂设计模式的情况...向下转型值得避免,多重继承除非在非常特定的情况下,但这是我的个人意见。 - AJG85
1个回答

8
那个函数指针转换(将返回 Both* 的函数视为返回 Verify* 或 Enroll*)是不正确的;在多重继承中,对象内的不同基类不一定从对象的开头开始。函数指针转换没有进行所需的偏移操作,用于从 Both* 到基础指针类型的派生到基础指针类型的指针转换。
您可以拥有一个单独的 createInstanceOfBoth 函数,但它需要返回 Both*;然后调用它的代码将使用该结果进行从派生到基础的转换。

这太有道理了!我已经为这个小细节苦苦思索了几个小时,但是现在你解释了一下,它显然很明显。谢谢! - RTBarnard
好的解释。“对象中不同的基类不一定从对象的开头开始”- 只有一个可以,所以这个说法有点保守;-) - Tony Delroy
@Tony:它们中的任何一个是否需要从派生对象的开头开始?我相信单一继承需要,但对于多重继承我不确定。 - Jeremiah Willcock
我不知道... 我以为你的意思是可能没有2+,所以我在想不可能有,但是这是一个好问题,是否需要1。 我没有看到标准禁止编译器放置某些内容的特殊原因,鉴于虚拟函数意味着它不是POD类型,不需要与来自C或硬件设备世界的任何东西具有布局兼容性。 - Tony Delroy
2
我还不明白如何调用正确的函数。 - Phil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接