2016-07-29 7 views
3

Возможно ли изменить наблюдаемое поведение программы простым добавлением новой виртуальной функции в базовый класс? Я имею в виду, что никакие другие изменения не должны вноситься в код.Можно ли сломать код, добавив новую виртуальную функцию в базовый класс?

+2

Конечно. Список возможных способов разлома существующего кода на C++ - это бесконечный список. –

+0

@SamVarshavchik Этот вопрос и ответ - это иллюстрация [запрошена в другом месте] (http://stackoverflow.com/questions/38656443/what-is-the-opposite-of-c-override-final-specifier/38656692#comment64694369_38656558). – Leon

+1

Одним из примеров является добавление чистой виртуальной функции. Никакая реализация в инстанцированном производном классе не приведет к нарушению компиляции. – aichao

ответ

2
#include <stdlib.h> 

struct A { 
#if ADD_TO_BASE 
    virtual void foo() { } 
#endif 
}; 

struct B : A { 
    void foo() { } 
}; 

struct C : B { 
    void foo() { abort(); } 
}; 

int main() { 
    C c; 
    B& b = c; 
    b.foo(); 
} 

Без виртуальной функции базового класса b.foo() является невиртуальном вызов B::foo():

$ g++ virt.cc 
$ ./a.out 

С виртуальной в базовом классе это виртуальный вызов C::foo():

$ g++ virt.cc -DADD_TO_BASE 
$ ./a.out 
Aborted (core dumped) 

Вы также можете получить неприятное неопределенное поведение из-за бинарной несовместимости, поскольку добавление виртуальной функции к не-полимологу Основной класс rphic изменяет его размер и макет (требуя, чтобы все другие единицы перевода, которые его использовали для перекомпиляции).

Добавление новой виртуальной функции в уже полиморфный базовый класс изменяет компоновку таблицы vtable, добавляя новую запись в конце или изменяя положение других функций, если она добавлена ​​посередине (и даже если она добавлена ​​в конец базы vtable, которая находится в середине vtable для любых производных классов, которые добавляют новые виртуальные функции). Это означает, что уже скомпилированный код, который использует vtable, может вызвать неправильную функцию, потому что он использует неправильный слот в таблице vtable.

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

5

Следующая программа печатает OK. Раскомментируйте виртуальную функцию в B и она начнет печать CRASH!.

#include <iostream> 

struct B 
{ 
    //virtual void bar() {} 
}; 

struct D : B 
{ 
    void foo() { bar(); } 
    void bar() { std::cout << "OK" << std::endl; } 
}; 

struct DD : D 
{ 
    void bar() { std::cout << "CRASH!" << std::endl; } 
}; 

int main() 
{ 
    DD d; 
    d.foo(); 
    return 0; 
} 

Проблема заключается в том, что после того, как виртуальная функция B::bar() вводится связывание вызова в bar()D::foo() изменяется от статического до динамического.

+2

Но это цель «виртуальных» функций в первую очередь ... Здесь нет ничего удивительного :-). Поскольку у вас есть сигнатура функции в производном классе, которая является такой же, но «виртуальной», как в одной из базовых (ов), она автоматически переопределяется. Ошибки от немного разных подписей частично связаны с тем, что вызвало ключевое слово 'override' – WhiZTiM

+0

@WhiZTiM. Этот вопрос и ответ - это иллюстрация [запрошенная в другом месте] (http://stackoverflow.com/questions/38656443/what-is-the-opposite- из-с-переопределение-конечная спецификатор/38656692 # comment64694369_38656558). – Leon

+1

@WhiZTiM: Нет ничего удивительного, но это * дает пример, доказывающий, что введение виртуальной функции в базовый класс может сломать предыдущий рабочий код (который является вопросом, который задал ОП). –

5

Двоичная несовместимость.

Если у вас есть модуль с внешней загрузкой (например, DLL), который использует старое определение базового класса, у вас возникнут проблемы. Или, если программа загрузчика имеет старое определение, а в DLL - новая, это та же проблема. Это также проблема, если вы по какой-то причине сохраняете объекты в файлах, используя необработанное двоичное копирование (а не какую-либо сериализацию).

Это не имеет никакого отношения к спецификации виртуальных функций C++, но как большинство компиляторов реализуют их.

Вообще говоря, если «интерфейс» класса изменяется (базовый класс или нет), вы должны перекомпилировать все, что использует этот класс.

+0

Это хуже, см. Ответ от Leon - который не требует ничего вне стандарта C++. –

2

Когда API изменен несовместимым образом, код, который зависит от более ранней версии API, больше не будет работать.

Все производные классы зависят от API их базовых классов.

Добавление виртуальной функции является обратным несовместимым изменением. Leon's Ответ показывает прекрасный пример того, как может проявиться разлом API.

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

+0

Вы утверждаете, что добавление виртуальной функции является обратным несовместимым изменением, но вы не предоставляете никаких доказательств этого утверждения. Учитывая, что вопрос может быть перефразирован как «добавление виртуальной функции к базовому классу обратно несовместимых изменений», просто сказать «да» не очень полезно. –

+0

@MartinBonner, что изменение изменит смысл вопроса (обратно несовместимое изменение, если хотите), хотя и тонко. Я согласен, мое утверждение должно сопровождаться примером. Леон уже опубликовал прекрасный пример, и я не вижу необходимости повторять его. Однако я буду ссылаться на это, чтобы поддержать мое утверждение. – user2079303