Должна ли виртуальная диспетчеризация происходить, когда виртуальный метод вызывается внутри виртуального метода с использованием объекта? - PullRequest
0 голосов
/ 18 августа 2011
struct B
{
  virtual void bar () {}
  virtual void foo () { bar(); }
};

struct D : B
{
  virtual void bar () {}
  virtual void foo () {}
};

Теперь мы вызываем foo(), используя объект B as,

B obj;
obj.foo();  // calls B::bar()

Вопрос : Если bar() будет разрешено с помощью virtual диспетчеризации, или оно будет разрешено с использованием статического типа объекта (т. Е. B).

Интересное обновление :

Если компилятору разрешено разрешать тип объекта, тогда может существовать неписанное правило:

Независимо от того, foo() вызывается с использованием объекта или указателя / ссылки, все вызовы внутри любого virtual B::foo() должны разрешаться статически. Потому что, когда вы находитесь внутри B::foo(), он уверен, что this имеет тип B*, а не D*.

Ответы [ 5 ]

3 голосов
/ 18 августа 2011

РЕДАКТИРОВАТЬ: Я думаю, что я неправильно понял ваш вопрос.Я почти уверен, что это зависит от того, насколько умным является оптимизатор компилятора.Наивная реализация, конечно, все равно будет проходить виртуальный поиск.Единственный способ узнать наверняка для конкретной реализации - это скомпилировать код и посмотреть на разборку, чтобы увидеть, достаточно ли он умен, чтобы сделать прямой вызов.

Оригинальный ответ:

быть фактически отправлен.Это становится более очевидным, если учесть, что в методе класса вызов метода работает с чем-то вроде this->bar();, что делает очевидным, что для вызова метода используется указатель, позволяющий использовать тип динамического объекта.

Однако в вашем примере, поскольку вы создали B, он, конечно, вызовет версию метода B.

Обратите внимание (как видно в комментарии), что виртуальная отправка не происходитвнутри конструкторов, даже используя неявное this->.

EDIT2 для вашего обновления: это совсем не так.Вызовы внутри B::foo() обычно не могут быть связаны статически (кроме случаев, когда компилятор знает статический тип объекта).Просто потому, что он знает, что он вызывается для B*, ничего не говорит о реальном типе рассматриваемого объекта - это может быть D* и ему требуется виртуальная диспетчеризация.

2 голосов
/ 18 августа 2011

Ответ: с языковой точки зрения вызов на bar() внутри B::foo() разрешается посредством виртуальной отправки.

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

some_object_ptr->SomeClass::some_method();
some_object.SomeClass::some_method();

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

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

B obj;
obj.foo();

метод вызывается через механизм «виртуальной отправки» независимо от контекста («внутри виртуального метода» или нет - не имеет значения).

Вот так на языке C ++. Все остальное - просто оптимизации, сделанные компиляторами. Как вы, вероятно, знаете, большинство (если не все) компиляторы будут генерировать не виртуальный вызов виртуального метода, когда вызов выполняется через непосредственный объект. Это, конечно, очевидная оптимизация, так как компилятор знает, что статический тип объекта совпадает с его динамическим типом. Опять же, это не зависит от контекста («внутри виртуального метода» или нет - не имеет значения).

Внутри виртуального метода вызов может быть выполнен без указания объекта с левой стороны (как в вашем примере), что действительно означает, что все вызовы this-> неявно присутствуют слева. Те же самые правила применяются и в этом случае. Если вы просто позвоните bar(), он будет означать this->bar(), и вызов будет отправлен практически. Если вы звоните B::bar(), это означает this->B::bar(), и вызов отправляется не виртуально. Все остальное будет зависеть только от возможностей оптимизации компилятора.

То, что вы пытаетесь сказать "потому что, как только вы попадаете внутрь B::foo(), оно точно относится к типу B*, а не D*", мне совершенно неясно. Это утверждение упускает суть. Виртуальная отправка зависит от типа dynamic объекта . Примечание: это зависит от типа *this, а не от типа this. Неважно, что это за тип this. Важен динамический тип *this. Когда вы находитесь внутри B::foo, все еще вполне возможно, что динамический тип *this равен D или что-то еще. По этой причине вызов bar() должен быть разрешен динамически.

2 голосов
/ 18 августа 2011

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

Обратите внимание, что это предполагает, что вы компилируете их отдельно. Если компилятор встроит вызов функции foo () (из-за известного статического типа), он также встроит вызов bar ().

1 голос
/ 18 августа 2011

В этом случае:

B obj;
obj.foo();  // calls B::bar()

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

Однако внутри B::foo() вызов bar() должен вообще использовать виртуальную диспетчеризацию (хотя компилятор мог бы встроить вызов, и для этот конкретный экземпляр вызова мог бы снова оптимизировать виртуальную диспетчеризацию).В частности, это предложение, которое вы предложили:

Независимо от того, foo() вызывается с использованием объекта или указателя / ссылки, все вызовы внутри любого виртуального B::foo() должны быть статически разрешены.Потому что, как только вы находитесь внутри B :: foo (), он уверен, что это тип B*, а не D*

не соответствует действительности.

Учтите:

struct D2 : B
{
    // D2 does not override bar()
    virtual void foo () {
        cout << "hello from D2::bar()" << endl;
    }
};

Теперь, если у вас где-то было следующее:

D2 test;

B& bref = test;

bref.foo();

Этот вызов foo() закончился бы B::foo(), но когда B::foo() вызывает bar(), оннужно отправить на D2::bar().

На самом деле, теперь, когда я это напечатал, B& совершенно не нужен для этого примера.

1 голос
/ 18 августа 2011

Целиком до реализации.Номинально это виртуальный вызов, но вы не вправе предполагать, что переданный код будет фактически выполнять перенаправление через vtable или подобное.

Если foo() вызывается на некотором произвольном B*, то изКонечно, код, выданный для foo(), должен сделать виртуальный вызов bar(), так как ссылка может принадлежать производному классу.

Это не произвольный B*, это объектдинамический тип B.Результат виртуального или не виртуального вызова точно такой же, поэтому компилятор может делать то, что ему нравится (правило «как если бы»), и соответствующая программа не может определить разницу.

Конкретнов этом случае, если вызов foo является встроенным, то я бы подумал, что оптимизатор имеет все шансы де-виртуализировать вызов внутри bar, так как он точно знает, что находится в vtable (или эквивалентном) obj.Если вызов не является встроенным, то он будет использовать «ванильный» код foo(), который, конечно, должен будет выполнять какую-то косвенную переадресацию, поскольку это тот же код, который используется при вызове произвольного B*.

...