Ключевое слово c++ "virtual" для функций в производных классах. Это необходимо?

с определением структуры, приведенным ниже...

struct A {
    virtual void hello() = 0;
};

подход #1:

struct B : public A {
    virtual void hello() { ... }
};

подход #2:

struct B : public A {
    void hello() { ... }
};

есть ли разница между этими двумя способами переопределения функции hello?

8 ответов


Они точно такие же. Нет никакой разницы между ними, кроме того, что первый подход требует большего набора текста и потенциально более ясен.


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

С чисто стилистической точки зрения, в том числе virtual ключевое слово явно "рекламирует" тот факт, что функция является виртуальной. Это будет важно для любого дальнейшего подкласса B без необходимости проверки определения A. Для глубоких иерархий классов, это становится особенно важно.


на virtual ключевое слово не нужно в производном классе. Вот вспомогательная документация, из проекта стандарта C++ (N3337) (акцент мой):

виртуальные функции 10.3

2 если виртуальная функция-член vf объявляется в классе Base и Derived, полученной прямо или косвенно от Base функции-члена vf с тем же именем, параметром-типом-списком (8.3.5), cv-квалификацией и ref-квалификатор (или его отсутствие) как Base::vf объявляется, то Derived::vf также является виртуальным (объявлено ли это так) и он переопределяет Base::vf.


нет,virtual ключевое слово для переопределения виртуальных функций производных классов не требуется. Но стоит упомянуть о связанной с этим ловушке: неспособности переопределить виртуальную функцию.

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

эта ловушка, однако, к счастью, устранена C++11 явное переопределение функция языка, которая позволяет исходному коду четко указать, что функция-член предназначена для переопределения функции базового класса:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

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

смотрите WP:C++11.


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


нет никакой разницы для компилятора, когда вы пишите virtual в производном классе или опустить ее.

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


есть большая разница, когда у вас есть шаблоны и начать принимать базовый класс(ы) в качестве параметра шаблона(ы):

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

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

обратите внимание, что, если вы это сделаете, вы также можете объявить конструкторы копирования / перемещения шаблонами: разрешение строить из разных интерфейсов позволяет вам "бросать" между разными B<> типы.

сомнительно, следует ли добавлять поддержку для const A& на t_hello(). Обычная причина этой перезаписи - переход от специализации на основе наследования к специализации на основе шаблонов, в основном по соображениям производительности. Если вы продолжаете поддерживать старый интерфейс, вы вряд ли можете обнаружить (или препятствуют) использование.


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