С++ вызов совершенно неправильного (виртуального) метода объекта

У меня есть код С++ (написанный кем-то другим), который, как представляется, вызывает неправильную функцию. Здесь ситуация:

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, он успешно зарегистрировал дочерний класс и его функцию.