С++ вызов совершенно неправильного (виртуального) метода объекта
У меня есть код С++ (написанный кем-то другим), который, как представляется, вызывает неправильную функцию. Здесь ситуация:
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);
Файлы, для которых выполняется условие true, обрабатываются штрафом; те, для которых это ложная ошибка. Иерархия классов для 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 С++ показывает, что вызов readDocument на BadDocumentReader вызывает не
readDocument(InputStream&, const wchar_t*)
а скорее
readDocument(const LocatedString* source, const wchar_t *, Symbol)
Это подтверждается приложением инструкций cout во всех readDocuments. После вызова исходный аргумент, конечно, полон мусора, что вскоре вызывает крах. У LocationString есть конструктор с несимметричным конструктором из InputStream, но проверка с помощью cout показывает, что он не получает вызов. Любая идея, что могло бы объяснить это?
Изменить: другие, возможно, релевантные детали: классы DocumentReader находятся в другой библиотеке, кроме кода вызова. Я также сделал полную перестройку всего кода, и проблема осталась.
Изменить 2. Я использую Visual С++ 2008.
Изменить 3. Я попытался создать "минимально компилируемый пример" с тем же поведением, но не смог реплицировать проблему.
Изменить 4:
В предложении Billy ONeal я попытался изменить порядок методов readDocument в заголовке BadDocumentReader. Разумеется, когда я меняю порядок, он меняет, какая из функций вызывается. Мне кажется, что я подтверждаю мое подозрение, что что-то странное происходит с индексированием в 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. Затем он помещает первое в vtable в edx и вызывает его. Перекомпиляция с разными порядками методов, похоже, не изменяет этого. Он всегда хочет назвать первое в vtable. (Я мог бы совершенно неправильно понять это, не имея знаний о собрании вообще))
Спасибо за вашу помощь.
Изменить 6: Я нашел проблему, и извиняюсь за то, что тратил все время. Проблема заключалась в том, что GoodDocumentReader должен был быть объявлен как подкласс DocumentReader, но на самом деле этого не было. Стили C-стиля подавляли ошибку компилятора (должны были выслушать вас, @sellibitze, если вы хотите отправить свой комментарий в качестве ответа, я буду отмечать его как правильно). Трудность в том, что код работал в течение нескольких месяцев с чистой случайностью до пересмотра, когда кто-то добавил еще две виртуальные функции в GoodDocumentReader, поэтому он больше не вызывал правильную функцию на удачу.
Ответы
Ответ 1
Я бы попытался сначала удалить C-cast.
- Это совершенно необязательно, отливки из Derived to 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
Это происходит потому, что разные исходные файлы не согласны с компоновкой vtable класса. Код, вызывающий функцию, считает, что readDocument(InputStream &, const wchar_t *)
находится на определенном смещении, в то время как фактическая vtable имеет его с другим смещением.
Обычно это происходит при изменении vtable, например, путем добавления или удаления виртуального метода в этом классе или любого из его родительских классов, а затем вы перекомпилируете один исходный файл, но не другой исходный файл. Затем вы получаете несовместимые объектные файлы, и когда вы их связываете, дела идут бум.
Чтобы исправить это, выполните полную очистку и перестройку всего кода: как код библиотеки, так и код, который использует библиотеку. Если у вас нет исходного кода в библиотеке, но у вас есть файлы заголовков для него с определениями классов, то это не вариант. В этом случае вы не можете изменить определение класса - вы должны вернуть его к тому, как он был предоставлен вам, и перекомпилировать весь ваш код.
Ответ 3
Основываясь на сборке, кажется довольно очевидным, что привязка динамическая и от первой записи vtable. Вопрос в том, какая виртуальная таблица!?! Я бы предложил использовать static_cast
вместо C-стиля (конечно, @VJo: dynamic_cast
в этом случае не требуется!). В стандарте нет ничего, что требует, чтобы указатель BadDocumentReader* ptr
имел то же фактическое значение (адрес), что и его литье static_cast<DocumentReader*>(ptr)
. Это объясняет, почему он связывает вызов первой записи vtable BadDocumentReader
, а не с vtable своего базового класса. И, кстати, в этом случае вам не нужно будет бросать.
Одна возможность, которая на самом деле не согласуется с asm, но все же хороша для понимания.
Поскольку вы создаете BadDocumentReader
в той же области, в которой вы вызываете reader->readDocument
, компилятор становится слишком умным и решает, что он может разрешить вызов без необходимости динамически искать его в виртуальной таблице. Это потому, что он знает, что "реальный" тип указателя читателя на самом деле BadDocumentReader
. Таким образом, он bipasses vtable и связывает вызов статически. По крайней мере, это одна из возможностей, с которой я столкнулся со мной в почти идентичной ситуации. Однако, основываясь на asm, я уверен, что первая возможность - это то, что происходит в вашем случае.
Ответ 4
У меня была эта проблема, и проблема для меня заключалась в том, что я сохранял ее в переменной класса. Когда я изменил его на указатель и включил new/delete, он успешно зарегистрировал дочерний класс и его функцию.