C ++ вызывает совершенно неправильный (виртуальный) метод объекта - PullRequest
15 голосов
/ 26 января 2011

У меня есть некоторый код 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 ++ показывает, что вызов readDocument для BadDocumentReaderвызов не

readDocument(InputStream&, const wchar_t*)

, а

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

Это подтверждается введением операторов cout во все readDocuments.После вызова аргумент источника, конечно, полон мусора, который вскоре вызывает сбой.В LocationString есть неявный конструктор с одним аргументом из InputStream, но проверка cout показывает, что он не вызывается.Любая идея, что могло бы объяснить это?

Редактировать : другие, возможно, важные детали: классы DocumentReader находятся в другой библиотеке, чем код вызова.Я также полностью перестроил весь код, и проблема осталась.

Редактировать 2 : Я использую Visual C ++ 2008.

Редактировать 3: я попытался создать «минимально компилируемый пример» с тем же поведением, но не смог воспроизвести проблему.

Edit 4 :

У Билли ONeal'sпредложение, я попытался изменить порядок методов readDocument в заголовке BadDocumentReader.Конечно, когда я меняю порядок, он меняет, какая из функций вызывается.Мне кажется, это подтверждает мое подозрение, что с индексацией в vtable происходит что-то странное, но я не уверен, что его вызывает.

Edit 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.Затем он помещает элемент first в vtable в edx и вызывает его.Перекомпиляция с различными порядками методов, кажется, не меняет этого.Всегда хочется позвонить первым делом в vtable.(Я мог бы совершенно неправильно понять это, вообще не имея представления о сборке ...)

Спасибо за вашу помощь.

Редактировать 6: Я нашел проблему,и я прошу прощения за тратить время всех.Проблема заключалась в том, что GoodDocumentReader должен был быть объявлен как подкласс DocumentReader, но на самом деле это не так.Приведения в стиле C подавляли ошибку компилятора (должен был вас выслушать, @sellibitze, если вы хотите отправить свой комментарий в качестве ответа, я отмечу его как правильный).Хитрость заключается в том, что код работал в течение нескольких месяцев по чистой случайности, до тех пор, пока кто-то не добавил еще две виртуальные функции в GoodDocumentReader, чтобы он больше не вызывал нужную функцию по счастливой случайности.

Ответы [ 4 ]

13 голосов
/ 26 января 2011

Это происходит потому, что разные исходные файлы не совпадают с макетом таблицы. Код, вызывающий функцию, думает, что readDocument(InputStream &, const wchar_t *) имеет конкретное смещение, тогда как фактический vtable имеет его с другим смещением.

Это обычно происходит, когда вы меняете виртуальную таблицу, например, добавляя или удаляя виртуальный метод в этом классе или любом из его родительских классов, а затем перекомпилируете один исходный файл, но не другой исходный файл. Затем вы получаете несовместимые объектные файлы, и, когда вы связываете их, дела идут быстро.

Чтобы исправить это, выполните полную очистку и перестройку всего своего кода: как кода библиотеки, так и кода, который использует библиотеку. Если у вас нет исходного кода для библиотеки, но у вас есть заголовочные файлы для него с определениями классов, тогда это не вариант. В этом случае вы не можете изменить определение класса - вам следует вернуть его к тому, как оно было вам дано, и перекомпилировать весь ваш код.

3 голосов
/ 26 января 2011

Я бы сначала попытался удалить 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
0 голосов
/ 24 февраля 2017

У меня была эта проблема, и проблема для меня заключалась в том, что я хранил ее в переменной члена класса.Когда я изменил его на указатель и включил new / delete, он успешно зарегистрировал дочерний класс и его функцию.

0 голосов
/ 26 января 2011

Исходя из сборки, кажется довольно ясным, что привязка является динамической и с первой записи vtable.Вопрос в том, какая виртуальная таблица!?!Я бы предложил вам использовать static_cast вместо приведения в стиле C (конечно, @VJo: dynamic_cast в этом случае не требуется!).В стандарте нет ничего, что требовало бы, чтобы указатель BadDocumentReader* ptr имел такое же фактическое значение (адрес), как и его приведение static_cast<DocumentReader*>(ptr).Это объясняет, почему он связывает вызов с первой записью виртуальной таблицы BadDocumentReader, а не с виртуальной таблицей ее базового класса.И, между прочим, в этом случае вам вообще не понадобится приведение.

Одна возможность, которая на самом деле не соответствует асму, но все же полезно знать.Поскольку вы создаете BadDocumentReader в той же области, в которой вы вызываете reader->readDocument, компилятор становится слишком умным и решает, что он может разрешить вызов без необходимости динамического поиска в виртуальной таблице.Это потому, что он знает, что «реальный» тип указателя читателя на самом деле BadDocumentReader.Таким образом, он обходит виртуальную таблицу и статически связывает вызов.По крайней мере, это одна возможность, с которой я столкнулся в почти идентичной ситуации.Однако, основываясь на асме, я почти уверен, что первая возможность - та, которая возникает в вашем случае.

...