Использование возможностей виртуальных функций

Рассмотрим следующий пример кода:

class Base {
public:
    void f();
    virtual void vf();
};

class Derived : public Base {
public:
    void f();
    void vf();
};

#include <iostream>
using namespace std;

void Base::f() {
    cout << "Base f()" << endl;
}

void Base::vf() {
    cout << "Base vf()" << endl;
}

void Derived::f() {
    cout << "Derived f()" << endl;
}

void Derived::vf() {
    cout << "Derived vf()" << endl;
}

int main()
{
    Base b1;
    Derived d1;
    b1.f();
    b1.vf();
    d1.f();
    d1.vf();

    Derived d2;     // Derived object
    Base* bp = &d2; // Base pointer to Derived object
    bp->f();    // Base f()
    bp->vf();   // which vf()?

    return 0;
}

Результат запуска:

Base f()

Base vf()

Derived f()

Derived vf()

Base f()

Derived vf()

Вопросы:

  1. В строке Base* bp = &d2 тип объекта известен во время компиляции. Тогда решение о том, какую функцию использовать в случае bp->vf();, также можно принять во время компиляции, верно?

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


person nitin_cherian    schedule 14.12.2011    source источник
comment
До компилятора. Кажется, нет смысла рассуждать об этом.   -  person Lightness Races in Orbit    schedule 14.12.2011
comment
@TomalakGeret'kal: Значит, в этом примере мощность виртуальных функций не используется, верно?   -  person nitin_cherian    schedule 14.12.2011
comment
Этот вопрос не имеет особого смысла. Язык точно предписывает, какое поведение вы получаете, и это именно то, что вы видите. Что тут непонятного? Лучше спросить о стоимости динамической отправки.   -  person Kerrek SB    schedule 14.12.2011
comment
@KerrekSB: Мой вопрос в том, действительно ли программа использует мощь виртуальных функций, которые являются динамическим связыванием?   -  person nitin_cherian    schedule 14.12.2011
comment
Я думаю, что @LinuxPenseur спрашивает во второй части: будет ли для этого примера создана таблица указателей виртуальных функций?   -  person Lee Netherton    schedule 14.12.2011
comment
@ltn100 ltn100: Да, это тоже можно сказать так.   -  person nitin_cherian    schedule 14.12.2011
comment
Единственное, что имеет значение, — это воспринимаемое действие во время выполнения. Стандарт определяет, как должны выполняться виртуальные функции, поэтому вы можете ожидать определенного поведения во время выполнения. В этом смысле сила виртуальных функций сохраняется, когда вы видите ожидаемый результат. Компилятор, с другой стороны, может делать оптимизации, когда это возможно, пока конечный результат тот же.   -  person Dave Rager    schedule 14.12.2011
comment
LinuxPenseur: программа должна действовать так, как если бы она работала, хотя под ней можно использовать ярлыки, если вы не видите разницы. Итак, простой ответ: да, он использует vtable.   -  person Mooing Duck    schedule 14.12.2011
comment
Ваш вопрос не в том направлении. Учитывая ожидаемое поведение программы, вы можете придумать наиболее общую реализацию, которая всегда будет работать. Затем вы можете спросить, требует ли ваша конкретная программа наиболее общей реализации для динамической диспетчеризации, или же компилятор сможет использовать статическую информацию для оптимизации...   -  person Kerrek SB    schedule 14.12.2011


Ответы (4)


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

// add second derived class
class Derived2 : public Base {
public:
    void vf() { std::cout << "Derived2 vf()" << std::endl; }
};

// in main
int user_choice;
std::cin >> user_choice;
Base * ptr;
if (user_choice == 0)
    ptr = new Derived();
else
    ptr = new Derived2();
ptr->vf();

Здесь выбор класса зависит от пользовательского ввода — компилятор не может предсказать, на какой тип объекта ptr будет фактически указывать при вызове ptr->vf();.

person Björn Pollex    schedule 14.12.2011

В строке Base* bp = &d2 тип объекта известен во время компиляции. Тогда решение о том, какую функцию использовать в случае bp->vf();, также можно принять во время компиляции, верно?

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

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

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

Полностью зависит от компилятора. Большинство современных компиляторов смогут оценить вызов этой функции во время компиляции.

person Alok Save    schedule 14.12.2011

Ну... ДА и НЕТ на оба вопроса: это зависит от того, на каком уровне абстракции вы рассуждаете.

  • С точки зрения языка, 1) является заблуждением. Тип для d2 известен, но когда придет адрес d2, который будет присвоен bp, произойдет преобразование из Derived* в Base*. С этого момента статический тип bp равен Base* (поскольку это его объявление), а динамический тип указывает на его Derived (поскольку это то, на что ссылается информация о типе во время выполнения, связанная с объектом). Начиная с них, каждая операция через bp предполагает тип Base* и требует перенаправления для каждой виртуальной функции.

  • С точки зрения компилятора можно выполнить определенную оптимизацию, и поскольку во всей вашей функции ваш pb всегда указывает на Derived, виртуальное перенаправление можно фактически пропустить. Но это связано со структурой вашего конкретного примера, а не с особенностями языка.

person Emilio Garavaglia    schedule 14.12.2011

In the line Base* bp = &d2, the object type is known at compile time. Then the decision of which function to use in the case of bp->vf(); can also be made at compile time right?

Нет, решение о том, какую функцию использовать, принимается динамически во время выполнения на основе типа объекта, а не типа указателя/ссылки на этот объект. Это называется динамической привязкой. Компилятор хранит скрытый указатель с именем virtual pointer или vptr, который указывает на таблицу, называемую виртуальной таблицей. Для каждого класса будет одна виртуальная таблица с хотя бы одной виртуальной функцией (независимо от того, сколько объектов создано для класса). Виртуальная таблица содержит адреса виртуальных функций класса.

Since the object type is known at compile time itself, is the power of virtual functions used in this sample program?

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

void draw(Shape *bp)
{
    bp->draw();
}

В этом случае фактическим объектом может быть любая форма, производная от Shape. Но нарисованная форма зависит от фактического типа передаваемого объекта.

person Sanish Gopalakrishnan    schedule 14.12.2011
comment
Боюсь, ваш ответ на первый вопрос противоречит ответу Элса на тот же вопрос. Я думаю, что ответ Алса дан после некоторых исследований по оптимизации компиляции. Но ваше объяснение с примером кода можно объединить с примером Бьорна, чтобы лучше понять мощь виртуальных функций. Не могли бы вы проверить, является ли ваш первый ответ полностью правильным? - person nitin_cherian; 14.12.2011
comment
Я имел в виду с точки зрения программиста. Компилятор может оптимизировать вызов, но это может зависеть от уровня абстракции. - person Sanish Gopalakrishnan; 14.12.2011