При переопределении виртуальной функции-члена, почему переопределенная функция всегда становится виртуальной? - PullRequest
4 голосов
/ 18 декабря 2009

Когда я пишу так:

class A {
    public: virtual void foo() = 0;
}

class B {
    public: void foo() {}
}

... B :: foo () также становится виртуальным. В чем причина этого? Я ожидаю, что он будет вести себя как ключевое слово final в Java.

Добавить: Я знаю, что так работает и как работает vtable :) Вопрос в том, почему стандартный комитет C ++ не оставил открытия для непосредственного вызова B :: foo () и избежания поиска vtable.

Ответы [ 6 ]

9 голосов
/ 18 декабря 2009

Стандарт оставляет возможность открывать вызов B :: foo напрямую и избегать просмотра таблицы:

#include <iostream>

class A {
    public: virtual void foo() = 0;
};

class B : public A {
    public: void foo() {
        std::cout <<"B::foo\n";
    }
};

class C : public B {
    public: void foo() {
        std::cout <<"C::foo\n";
    }
};

int main() {
    C c;
    A *ap = &c;
    // virtual call to foo
    ap->foo();
    // virtual call to foo
    static_cast<B*>(ap)->foo();
    // non-virtual call to B::foo
    static_cast<B*>(ap)->B::foo();
}

Выход:

C::foo
C::foo
B::foo

Таким образом, вы можете получить ожидаемое поведение следующим образом:

class A {
    virtual void foo() = 0;
    // makes a virtual call to foo
    public: void bar() { foo(); }
};

class B : public A {
    void foo() {
        std::cout <<"B::foo\n";
    }
    // makes a non-virtual call to B::foo
    public: void bar() { B::foo(); }
};

Теперь абоненты должны использовать bar вместо foo. Если у них есть C *, то они могут привести его к A *, в этом случае bar вызовет C::foo, или они могут привести его к B *, и в этом случае bar вызовет B::foo. C может переопределить bar снова, если он этого хочет, или не беспокоиться, и в этом случае вызов bar() на C * вызывает B::foo(), как и следовало ожидать.

Хотя я не знаю, когда кому-нибудь захочется такого поведения. Весь смысл виртуальных функций заключается в том, чтобы вызывать одну и ту же функцию для данного объекта, независимо от того, какой указатель базового или производного класса вы используете. Поэтому в C ++ предполагается, что если вызовы определенной функции-члена через базовый класс являются виртуальными, то вызовы через производные классы также должны быть виртуальными.

6 голосов
/ 18 декабря 2009

Когда вы объявляете метод virtual, вы в основном добавляете новую запись в vtable. Переопределение метода virtual изменяет значение этой записи; это не удаляет это. Это в основном верно и для таких языков, как Java или C #. Разница в том, что с ключевым словом final в Java вы можете попросить компилятор произвольно принудительно установить , что он не может его переопределить. C ++ не предоставляет эту языковую функцию.

5 голосов
/ 18 декабря 2009

То, что класс вынужден иметь vtable, не означает, что компилятор вынужден его использовать. Если тип объекта известен статически, компилятор может обойти vtable в качестве оптимизации. Например, B :: foo, вероятно, будет вызываться непосредственно в этой ситуации:

B b;
b.foo();

К сожалению, единственный способ проверить это - посмотреть на сгенерированный код сборки.

2 голосов
/ 18 декабря 2009

Потому что технически это виртуально, что бы вы ни делали - оно занимает свое место в таблице. Остальное было бы синтаксическим правоприменением, и именно здесь C ++ отличается от java.

1 голос
/ 18 декабря 2009

Vtable создается для базового класса, когда определена первая виртуальная функция. В вашем примере foo () имеет запись в vtable. Когда производный класс наследует от базового класса, он также наследует vtable. Производный класс должен иметь запись для foo () в своей vtable, чтобы вызов был перенаправлен соответствующим образом, когда на производный класс полиморфно ссылаются через указатель базового класса.

0 голосов
/ 09 ноября 2014

Похоже, что по крайней мере Visual Studio может использовать ключевое слово final для пропуска поиска в vtable, например, такой код:

class A {
public:
    virtual void foo() = 0;
};
class B : public A {
public:
    void foo() final {}
};
B original;
B& b = original;
b.foo();
b.B::foo();

Создает один и тот же код для b.foo() и для b.B::foo():

        b.foo();
000000013F233AA9  mov         rcx,qword ptr [b]
000000013F233AAE  call        B::foo (013F1B4F48h)
        b.B::foo();
000000013F233AB3  mov         rcx,qword ptr [b]
000000013F233AB8  call        B::foo (013F1B4F48h)

Принимая во внимание, что без final используется таблица поиска:

        b.foo();
000000013F893AA9  mov         rax,qword ptr [b]
000000013F893AAE  mov         rax,qword ptr [rax]
000000013F893AB1  mov         rcx,qword ptr [b]
000000013F893AB6  call        qword ptr [rax]
        b.B::foo();
000000013F893AB8  mov         rcx,qword ptr [b]
000000013F893ABD  call        B::foo (013F814F48h)

Я не знаю, делают ли другие компиляторы то же самое.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...