Путаница с виртуальными функциями и производными классами

Я пытаюсь понять следующий фрагмент кода:

#include<iostream>
using namespace std;
class Base {
    public:
        virtual void f(float) { cout << "Base::f(float)\n"; }
};
class Derived : public Base {
    public:
        virtual void f(int) { cout << "Derived::f(int)\n"; }
};
int main() {
    Derived *d = new Derived();
    Base *b = d;
    d->f(3.14F);
    b->f(3.14F);
}

Это печатает

Derived::f(int)
Base::f(float)

И я не уверен, почему именно.

Первый вызов d->f(3.14F) вызывает функцию f в Derived. Я не уверен на 100%, почему. Я посмотрел на это (http://en.cppreference.com/w/cpp/language/implicit_cast), в котором говорится:

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

Что для меня говорит, что вы не можете этого сделать, так как float не вписывается в int. Почему разрешено это неявное преобразование?

Во-вторых, даже если я просто принимаю вышеизложенное как нормальное, второй вызов b->f(3.14F) не имеет смысла. b->f(3.14F) вызывает виртуальную функцию f, поэтому она динамически разрешается для вызова f(), связанного с динамическим типом объекта, на который указывает b, который является производным объектом. Поскольку нам разрешено преобразовывать 3.14F в int, поскольку первый вызов функции указывает, что это допустимо, это (насколько я понимаю) должно снова вызывать функцию Derived::f(int). Тем не менее, он вызывает функцию в базовом классе. Так почему это?

edit: вот как я это понял и объяснил себе.

b является указателем на базовый объект, поэтому мы можем использовать b только для доступа к членам базового объекта, даже если b действительно указывает на какой-либо производный объект (это стандартный объектно-ориентированный подход/наследование).

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

Теперь, в приведенном выше фрагменте кода, у нас не происходит переопределения, потому что сигнатуры B::f и D::f разные (одна — число с плавающей запятой, другая — целое число). Поэтому, когда мы вызываем b->f(3.14F), единственная рассматриваемая функция — это исходная B::f, которая и вызывается.


person michael Davies    schedule 28.10.2012    source источник
comment
Вы неправильно понимаете эту цитату. Значение после усечения должно быть значением, которое можно представить с помощью int. 3 подходит идеально. Вы получите неопределенное поведение для очень больших значений с плавающей запятой.   -  person Mat    schedule 28.10.2012


Ответы (3)


Две функции имеют разные сигнатуры, поэтому f в derived не переопределяет виртуальную функцию в base. Тот факт, что типы int и float могут быть неявно преобразованы, здесь не имеет значения.

virtual void f(float) { cout << "Base::f(float)\n"; }
virtual void f(int) { cout << "Derived::f(int)\n"; }

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

virtual void f(int) override { cout << "Derived::f(int)\n"; }

из чего gcc выдает ошибку:

virtual void Derived::f(int)’ помечен как переопределение, но не переопределяется

лязг

ошибка: «f» помечен как «переопределить», но не переопределяет функции-члены

http://en.cppreference.com/w/cpp/language/override

РЕДАКТИРОВАТЬ:

для вашего второго пункта вы можете фактически выставить перегрузку float из base в derived, которая предоставляет неявно совместимую функцию-член. вот так:

class Derived : public Base {
public:
    using Base::f;
    virtual void f(int) { cout << "Derived::f(int)\n"; }
};

Теперь передача числа с плавающей запятой в функцию-член f привязывается ближе к функции, определенной в базе, и производит:

Base::f(float)
Base::f(float)
person 111111    schedule 28.10.2012
comment
Таким образом, подписи отличаются из-за int/float, поэтому переопределения не происходит. Ok. Но я до сих пор не понимаю, почему это означает, что b->f(3.14) будет вызывать версию в базе. Если D::f скрывает B::f, то почему она не скрыта, когда мы вызываем b-›f. Разве тот факт, что функция B::f скрыта, не должен применяться независимо от того, обращаемся ли мы к f через указатель B или указатель D? - person michael Davies; 30.10.2012
comment
Хорошо, я понял это. Я думаю, это то, что вы говорили, но мне нужно было расшифровать все детали, чтобы я полностью понял это. Я отредактировал исходный пост, чтобы включить мое объяснение. - person michael Davies; 30.10.2012

Простой способ подумать о сокрытии выглядит следующим образом: посмотрите на строку d->f(3.14F); из примера:

  1. Первым шагом для компилятора является выбор имени класса. Для этого используется имя функции-члена f. Типы параметров не используются. Выбирается производное.
  2. Следующим шагом для компилятора является выбор функции-члена из этого класса. Используются типы параметров. недействительным производным::f(int); — единственная подходящая функция с правильным именем и параметрами из класса Derived.
  3. Происходит сужающее преобразование типа из float в int.
person tp1    schedule 28.10.2012

Поскольку типы аргументов этих двух функций различаются, тот, что в классе Derived, на самом деле не переопределяет аргумент из Base. Вместо этого Derived::f скрывает Base::f (сейчас у меня нет стандарта, поэтому я не могу процитировать главу).

Это означает, что когда вы вызываете d->f(3.14f), компилятор даже не рассматривает B::f. Он разрешает вызов D::f. Однако, когда вы вызываете b->f(3.14f), единственная версия, которую может выбрать компилятор, это B::f, поскольку D::f не переопределяет ее.

Ваше прочтение If the value can not fit into the destination type, the behavior is undefined неверно. Он говорит, что значение не тип. Таким образом, значение 3.0f подходит для int, а 3e11 — нет. В последнем случае поведение не определено. Первая часть вашей цитаты, A prvalue of floating-point type can be converted to prvalue of any integer type., объясняет, почему d->f(3.14f) разрешается в D::f(int) - float действительно может быть преобразован в целочисленный тип.

person Zdeslav Vojkovic    schedule 28.10.2012