关于重载:C ++调用对象的完全错误(虚拟)方法

C++ calling completely wrong (virtual) method of an object

我有一些C++代码(别人写的),它似乎在调用错误的函数。情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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的类层次结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

以下也是相关的:

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

在VisualC++调试程序中运行,它显示了BADDCONTRONTER读取器上的RealDebug调用不调用

1
readDocument(InputStream&, const wchar_t*)

而是

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

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

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

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

编辑3:我尝试用相同的行为制作一个"最低可编译的示例",但无法复制问题。

编辑4:

在BillyOneal的建议下,我尝试更改baddocumentreader头中readdocument方法的顺序。当然,当我更改顺序时,它会更改调用哪个函数。在我看来,这证实了我的怀疑,在vtable中索引有一些奇怪的事情发生,但我不确定是什么引起的。

编辑5:以下是函数调用前几行的反汇编:

1
2
3
4
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中添加了两个虚拟函数,所以它不再幸运地调用正确的函数。


这是因为不同的源文件在类的vtable布局上不一致。调用函数的代码认为readDocument(InputStream &, const wchar_t *)处于特定的偏移量,而实际vtable具有不同的偏移量。

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

要解决这个问题,请对所有代码进行完全清理和重建:库代码和使用库的代码。如果您没有库的源代码,但是您有类定义的头文件,那么这不是一个选项。在这种情况下,您不能修改类定义——您应该将它恢复为它是如何提供给您的,并重新编译所有代码。


我会先尝试移除C型铸件。

  • 这是完全不必要的,从派生到基的强制转换在语言中是自然的。
  • 实际上,它可能会导致一个错误(尽管不应该如此)

看起来像是个编译器错误…这肯定不是第一次在vs.

不幸的是,我手头没有与2008年相比,在海湾合作委员会的铸型正确发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 <<"
"
;

  return 0;
}


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


我有这个问题,问题是我将它存储在一个类成员变量中。当我将它更改为指针并涉及new/delete时,它成功地注册了子类及其函数。


基于程序集,似乎非常清楚绑定是动态的,并且从vtable的第一个条目开始。问题是哪个虚拟表!?!我建议您使用static_cast而不是c样式的演员表(当然,在这种情况下不需要@vjo:dynamic_cast)!.标准中没有要求指针BadDocumentReader* ptr具有与其cast static_cast(ptr)相同的实际值(地址)。这可以解释为什么它将调用绑定到BadDocumentReader的vtable的第一个条目,而不是它的基类的vtable。顺便说一句,在这种情况下你根本不需要演员表。

有一种可能性并不完全符合ASM,但仍然值得一提。因为您在调用reader->readDocument的相同范围内创建BadDocumentReader,所以编译器变得有点过于聪明,决定它可以在不需要动态查找vtable的情况下解析调用。这是因为它知道读卡器指针的"实际"类型实际上是BadDocumentReader。所以它对vtable进行双向访问并静态绑定调用。至少,这是我在几乎相同的情况下发生在我身上的一种可能性。不过,基于ASM,我很确定第一种可能性是发生在您的案例中的可能性。