Как компилятор C++ узнает, какую реализацию виртуальной функции следует вызвать?

Вот пример полиморфизма из http://www.cplusplus.com/doc/tutorial/polymorphism.html (отредактировано для удобства чтения):

// abstract base class
#include <iostream>
using namespace std;

class Polygon {
    protected:
        int width;
        int height;
    public:
        void set_values(int a, int b) { width = a; height = b; }
        virtual int area(void) =0;
};

class Rectangle: public Polygon {
    public:
        int area(void) { return width * height; }
};

class Triangle: public Polygon {
    public:
        int area(void) { return width * height / 2; }
};

int main () {
    Rectangle rect;
    Triangle trgl;
    Polygon * ppoly1 = &rect;
    Polygon * ppoly2 = &trgl;
    ppoly1->set_values (4,5);
    ppoly2->set_values (4,5);
    cout << ppoly1->area() << endl; // outputs 20
    cout << ppoly2->area() << endl; // outputs 10
    return 0;
}

Мой вопрос: как компилятор узнает, что ppoly1 - это прямоугольник, а ppoly2 - это треугольник, чтобы он мог вызвать правильную функцию area ()? Это можно было выяснить, посмотрев на "Polygon * ppoly1 = & rect;" line и зная, что прямоугольник - это прямоугольник, но это не сработает во всех случаях, не так ли? Что, если бы вы сделали что-то подобное?

cout << ((Polygon *)0x12345678)->area() << endl;

Предполагая, что вам разрешен доступ к этой случайной области памяти.

Я бы проверил это, но я не могу использовать компьютер, на котором сейчас работаю.

(Надеюсь, я не упускаю что-то очевидное ...)

Оффтоп: Почему бы не проголосовать за других людей, которые потратили время на написание полезных ответов для вас?

Mike F 15.10.2008 03:10
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
1
6 219
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Не обращая внимания на аспекты привязки, на самом деле это определяет не компилятор.

Это среда выполнения C++, которая оценивает с помощью vtables и vpointers, что на самом деле представляет собой производный объект во время выполнения.

Я настоятельно рекомендую книгу Скотта Мейера «Эффективный C++» за хорошее описание того, как это делается.

Даже описывает, как параметры по умолчанию в методе производного класса игнорируются, а любые параметры по умолчанию в базовом классе по-прежнему принимаются! Это обязывает.

Ответ принят как подходящий

Каждый объект (который принадлежит к классу, по крайней мере, с одной виртуальной функцией) имеет указатель, называемый vptr. Он указывает на vtbl его фактического класса (который каждый класс с виртуальными функциями имеет по крайней мере один из; возможно, более одного для некоторых сценариев множественного наследования).

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

В вашем конкретном случае у Polygon, Rectangle и Triangle есть vtbl, каждая с одной записью, указывающей на соответствующий метод area. Ваш ppoly1 будет иметь vptr, указывающий на Rectanglevtbl, и ppoly2 аналогично Trianglevtbl. Надеюсь это поможет!

vptr / vtbl. Ралли Не помню в стандарте :-) Указатель на vtable. Если vtable - это структура, определяемая компилятором, это более наглядно.

Martin York 15.10.2008 02:51

@Martin: vptr / vtbl - это термины, используемые в книге Бьярна Страуструпа «Язык программирования C++». :-)

Chris Jester-Young 15.10.2008 02:54

Я думаю, что vtable не требуется стандартом, просто так получилось, что большинство компиляторов реализуют полиморфизм, используя его, поэтому он стал более или менее стандартным поведением

1800 INFORMATION 15.10.2008 02:56

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

Michael Burr 15.10.2008 02:56

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

Chris Jester-Young 15.10.2008 02:58

Извините, не не согласен с вашим объяснением, я пытался сказать следующее: лексема 'vtbl' не очень информативна, и если вы еще не слышали термин vtable. Когда я обсуждаю эту тему с коллегами, мы используем термин vtable (никто не говорит о vtbl).

Martin York 15.10.2008 03:04

Думаю, почти все языки C++ и C++ используют vtables. См. Здесь: en.wikipedia.org/wiki/Vtable - в этом разделе говорится о языках с множественной отправкой, которым требуется что-то более продвинутое.

1800 INFORMATION 15.10.2008 04:06

Другой часто используемый метод - это хеш-таблица имен методов.

1800 INFORMATION 15.10.2008 04:08

Крик! Не хеш-таблица имен методов! Похоже на позднюю привязку IDispatch (или как там она называется, я имею в виду ту, которая ищет указатели функций по именам методов, а не по идентификаторам DispID). Приносим извинения, если вы никогда не играли с OLE Automation и не понимаете, о чем я говорю. :-П

Chris Jester-Young 15.10.2008 04:32

Чтобы ответить на вторую часть вашего вопроса: на этом адресе, вероятно, не будет v-table в нужном месте, и это приведет к безумию. Кроме того, он не определен в соответствии со стандартом.

Крис Джестер-Янг дает основной ответ на этот вопрос.

Википедия имеет более глубокую трактовку.

Если вы хотите знать полную информацию о том, как работает этот тип вещей (и для всех типов наследования, включая множественное и виртуальное наследование), одним из лучших ресурсов является "Внутри объектной модели C++" Стэна Липпмана.

cout << ((Polygon *)0x12345678)->area() << endl;

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

В C++ вы не должны использовать старые C-стили вроде этого, вы должны использовать dynamic_cast так:

Polygon *obj = dynamic_cast<Polygon *>(0x12345678)->area();
ASSERT(obj != NULL);

cout << obj->area() << endl;

dynamic_cast вернет NULL, если данный указатель не является допустимым объектом Polygon, поэтому он будет захвачен ASSERT.

Вы не можете использовать dynamic_cast из целого числа! (На самом деле вы также не можете использовать dynamic_cast из void *, вам нужно начать с указателя / ссылки на тип, который имеет какое-то отношение к типу, к которому вы выполняете приведение.)

Chris Jester-Young 15.10.2008 04:34

Итак, я хочу сказать, что со случайным адресом, таким как reinterpret_cast <void *> (0x12345678), вы находитесь в зоне неопределенного поведения, несмотря ни на что. :-П

Chris Jester-Young 15.10.2008 04:35

Я действительно делал такие вещи, чтобы хранить указатели объектов в списке Windows. По общему признанию, я должен написать это так: MYTYPE * obj = dynamic_cast <MYTYPE *> ((MYTYPE *) listbox.GetItemData (item)); Dynamic_cast немного безопаснее, чем прямое приведение.

Adam Pierce 15.10.2008 07:48

Если GetItemData возвращает void *, вы можете избежать приведений в стиле C, используя вместо этого static_cast <MYTYPE *>. :-) Некоторые люди, которых я знаю, весьма догматичны в отношении избегания приведений в стиле C, потому что они могут быть довольно грубым инструментом (например, могут вызывать reinterpret_cast в непредвиденных случаях).

Chris Jester-Young 15.10.2008 13:22

С другой стороны, если GetItemData возвращает int, то переинтерпретировать его в указатель не безопасно, и в этом случае я бы, вероятно, использовал map <int, MYTYPE *>. :-)

Chris Jester-Young 15.10.2008 13:23

Таблицы виртуальных функций. То есть, оба ваших объекта, производных от Polygon, имеют таблицу виртуальных функций, которая содержит указатели функций на реализации всех их (нестатических) функций; и когда вы создаете экземпляр Triangle, указатель виртуальной функции для функции area () указывает на функцию Triangle :: area (); когда вы создаете экземпляр Rectangle, функция area () указывает на функцию Rectangle :: area (). Поскольку указатели виртуальных функций хранятся вместе с данными для объекта в памяти, каждый раз, когда вы ссылаетесь на этот объект как на многоугольник, будет использоваться соответствующая область () для этого объекта.

Другие вопросы по теме