C ++调用对象的完全错误(虚拟)方法

时间:2011-01-25 21:20:10

标签: c++ overloading virtual-functions

我有一些C ++代码(由其他人编写)似乎调用了错误的函数。情况如下:

UTF8InputStreamFromBuffer* cstream = foo();
wstring fn = L"foo";
DocumentReader* reader;

if (a_condition_true_for_some_files_false_for_others) {
    reader = (DocumentReader*) _new GoodDocumentReader();
} else {
    reader = (DocumentReader*) _new BadDocumentReader();
}

// the crash happens inside the following call
// when a BadDocumentReader is used
doc = reader->readDocument(*cstream, fn);

条件为真的文件处理正常;它是虚假崩溃的那些。 DocumentReader的类层次结构如下所示:

class GenericDocumentReader {
    virtual Document* readDocument(InputStream &strm, const wchar_t * filename) = 0;
}

class DocumentReader : public GenericDocumentReader {
    virtual Document* readDocument(InputStream &strm, const wchar_t * filename) {
        // some stuff
    }
};

class GoodDocumentReader : public DocumentReader {
    Document* readDocument(InputStream & strm, const wchar_t * filename);
}

class BadDocumentReader : public DocumentReader {
    virtual Document* readDocument(InputStream &stream, const wchar_t * filename);
    virtual Document* readDocument(const LocatedString *source, const wchar_t * filename);
    virtual Document* readDocument(const LocatedString *source, const wchar_t * filename, Symbol inputType);
}

以下内容也是相关的:

class UTF8InputStreamFromBuffer : public wistringstream {
    // foo
};
typedef std::basic_istream<wchar_t> InputStream;

在Visual C ++调试器中运行,它显示BadDocumentReader上的readDocument调用不是

readDocument(InputStream&, const wchar_t*)

而是

readDocument(const LocatedString* source, const wchar_t *, Symbol)

通过在所有readDocuments中粘贴cout语句来确认这一点。在调用之后,source参数当然充满了垃圾,这很快就会导致崩溃。 LocationString确实有一个来自InputStream的单参数隐式构造函数,但是用cout检查表明它没有被调用。知道什么可以解释这个吗?

编辑:其他可能相关的详细信息:DocumentReader类与调用代码位于不同的库中。我还完成了所有代码的完全重建,问题仍然存在。

编辑2 :我正在使用Visual C ++ 2008。

编辑3 :我尝试使用相同的行为制作“最低限度可编辑的示例”,但无法复制问题。

编辑4

在Billy ONeal的建议下,我尝试更改BadDocumentReader标头中readDocument方法的顺序。果然,当我改变顺序时,它会改变调用哪些函数。在我看来,这证实了我的怀疑,因为索引到vtable有一些奇怪的事情,但我不确定是什么导致它。

编辑5 : 这是函数调用之前几行的反汇编:

00559728  mov         edx,dword ptr [reader] 
0055972E  mov         eax,dword ptr [edx] 
00559730  mov         ecx,dword ptr [reader] 
00559736  mov         edx,dword ptr [eax] 
00559738  call        edx  

我不太了解汇编,但它看起来像它取消引用读者变量指针。存储在这部分内存中的第一件事应该是指向vtable的指针,因此它将其解释为eax。然后它将第一个事物放在edx中的vtable中并调用它。使用不同的方法顺序重新编译似乎并没有改变这一点。它总是想要在vtable中调用第一件事。 (我完全可能误解了这一点,根本不了解装配......)

感谢您的帮助。

编辑6:我发现了问题,我为浪费每个人的时间而道歉。问题是GoodDocumentReader应该被声明为DocumentReader的子类,但事实上并非如此。 C风格的演员阵容抑制了编译错误(应该听你的,@ sellibitze,如果你想提交你的评论作为答案,我会把它标记为正确)。棘手的是,代码在纯粹的事故中已经工作了几个月,直到有人向GoodDocumentReader添加了两个虚函数,因此不再通过运气调用正确的函数。

4 个答案:

答案 0 :(得分:13)

这种情况正在发生,因为不同的源文件在类的vtable布局上存在分歧。调用该函数的代码认为readDocument(InputStream &, const wchar_t *)处于特定的偏移量,而实际的vtable则具有不同的偏移量。

这通常发生在更改vtable时,例如通过在该类或其任何父类中添加或删除虚方法,然后重新编译一个源文件而不是另一个源文件。然后,你会得到不兼容的目标文件,当你链接它们时,事情会变得繁荣。

要解决此问题,请完全清理并重建所有代码:库代码和使用该库的代码。如果您没有库的源代码,但是您确实拥有类定义的头文件,那么这不是一个选项。在这种情况下,您无法修改类定义 - 您应该将其恢复为给定的方式并重新编译所有代码。

答案 1 :(得分:3)

我会先尝试删除C-cast。

  • 完全没有必要,从Derived转换为Base是一种自然的语言
  • 实际上,它可能会导致错误(虽然它不应该)

它看起来像编译器错误......它肯定不会是VS中的第一个。

我很遗憾没有VS 2008,在gcc中,演员阵容正确发生:

struct Base1
{
  virtual void foo() {}
};

struct Base2
{
  virtual void bar() {}
};

struct Derived: Base1, Base2
{
};

int main(int argc, char* argv[])
{
  Derived d;
  Base1* b1 = (Base1*) &d;
  Base2* b2 = (Base2*) &d;

  std::cout << "Derived: " << &d << ", Base1: " << b1
                                 << ", Base2: " << b2 << "\n";

  return 0;
}


> Derived: 0x7ffff1377e00, Base1: 0x7ffff1377e00, Base2: 0x7ffff1377e08

答案 2 :(得分:0)

基于程序集,很明显绑定是动态的,并且来自vtable的第一个条目。问题是哪个虚拟表!?!我建议你使用static_cast而不是C风格的演员(当然,在这种情况下,不需要@VJo:dynamic_cast!)。标准中没有任何内容要求指针BadDocumentReader* ptr具有与其转换static_cast<DocumentReader*>(ptr)相同的实际值(地址)。这可以解释为什么它将调用绑定到BadDocumentReader的vtable的第一个条目而不是它的基类的vtable。而且,顺便说一句,在这种情况下你根本不需要演员。

一种可能与asm并不完全一致,但仍然很难知道。 因为您在调用BadDocumentReader的同一范围内创建reader->readDocument,所以编译器会变得有点过于聪明并且决定它可以解析调用而无需动态地在vtable中查找它。这是因为它知道读者指针的“真实”类型实际上是BadDocumentReader。因此它会对vtable进行双向化并静态绑定呼叫。至少,这是我在几乎完全相同的情况下发生在我身上的一种可能性。但是,基于asm,我很确定第一种可能性就是你的情况。

答案 3 :(得分:0)

我遇到了这个问题,问题在于我将它存储在类成员变量中。当我将其更改为指针并涉及new / delete时,它成功注册了子类及其功能。