Разница между использованием не виртуальных функций базового класса и нереализованных виртуальных функций производного класса - PullRequest
0 голосов
/ 11 ноября 2018

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

Вот небольшой фон. Допустим, у меня есть базовый класс A и два производных класса B и C

#include <iostream>

class A {
public:
  A() {};
  virtual void foo() { std::cout << "foo() called in A\n"; };
  virtual void bar() { std::cout << "bar() called from A\n"; };
  void xorp() { std::cout << "xorp() called from A\n"; };
  virtual ~A() {};
};

class B : public A {
public:
  B() {};
  virtual ~B() {};
  virtual void foo() override { std::cout << "foo() called in B\n"; };
  //virtual void bar() override not implemented in B, using A::bar();
};

class C : public A {
public:
  C() {};
  virtual ~C() {};
  virtual void foo() override { std::cout << "foo() called in C\n"; };
  //virtual bar() override not implemented in C, using A::bar();
};

int main() {
  A a{};
  B b{};
  C c{};

  a.foo(); //calls A::foo()
  a.bar(); //calls A::bar()
  a.xorp(); //calls non-virtual A::xorp()

  b.foo(); //calls virtual overridden function B::foo()
  b.bar(); //calls virtual non-overridden function A::bar()
  b.xorp(); //calls non-virtual A::xorp()

  c.foo(); //calls virtual overridden function C::foo()
  c.bar(); //calls virtual non-overridden function A::bar()
  c.xorp(); //calls non-virtual A::xorp()

  return 0;
}

Это выводит, как и ожидалось, следующее:

foo() called in A
bar() called from A
xorp() called from A
foo() called in B
bar() called from A
xorp() called from A
foo() called in C
bar() called from A
xorp() called from A

Если я оставлю виртуальную функцию bar () не реализованной в производных классах, любой вызов bar () в производных классах B и C будет преобразован в A :: bar (). xorp (), которая не является виртуальной функцией, также может вызываться из производных классов как b.xorp () или b.A :: xorp ().

Если бы я реализовал, например, xorp () в B, он бы эффективно скрывал A :: xorp (), а вызов b.xorp () фактически был бы вызовом bB :: xorp () .

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

Есть ли разница между тем, что вспомогательная функция является не виртуальной функцией-членом (например, xorp ()), и вспомогательной функцией, являющейся виртуальной функцией, которую производные классы не переопределяют (bar ()) ?

Прочитав эту презентацию о расположении объектов класса и VTABLE (https://www.cs.bgu.ac.il/~asharf/SPL/Inheritance.pptx, слайды 28-35), я не смог заметить разницу, поскольку как виртуальные, так и не переопределенные виртуальные функции указывают на одно и то же место ( т.е. функция в базовом классе)

Может ли кто-нибудь дать мне пример, где эти два подхода дали бы разные результаты, или если есть предупреждение, которое я не заметил?

Ответы [ 2 ]

0 голосов
/ 11 ноября 2018

Есть ли разница между тем, что вспомогательная функция является не виртуальной функцией-членом (например, xorp ()), и вспомогательной функцией, являющейся виртуальной функцией, которую производные классы не переопределяют (bar ())?

Если вы пометите метод как виртуальный, но никогда не переопределите его, его поведение будет эквивалентно тому, если вы никогда не пометили его как виртуальный. Отношение к другим методам, которые он вызывает в объекте, не изменяется.

Это не значит, что до сих пор нет "различий".

Это, безусловно, передает разницу в намерениях тем, кто читает код. Если ваш метод xorp () не является виртуальным - но использует виртуальные методы для реализации его поведения - тогда люди будут понимать «xorpiness» как наличие определенных фиксированных свойств. Скорее всего, они попытаются избежать переопределения xorp () в любых производных классах и будут знать, что косвенно влияют только на xorpiness путем определения виртуальных методов, от которых оно зависит.

Кроме того, компилятор не всегда может знать, собираетесь ли вы использовать виртуальное переопределение или нет. Поэтому он не может оптимизировать дополнительный код для виртуальной отправки - даже если вы не «используете» его. (Иногда это возможно, например, если у вас есть класс, из которого вы никогда не производите и не экспортируете его, виртуальный может просто исчезнуть.)

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

0 голосов
/ 11 ноября 2018

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

void baz(A& a) {
  a.foo();
  a.bar();
  a.xorp();
}

int main() {
  // As before
  baz(a);
  baz(b);
  baz(c);
}

Теперь вы сможете увидеть заметную разницу в том, как разрешаются звонки на foo, bar и baz. В частности ...

Если бы я реализовал, например, xorp () в B, он бы эффективно скрывал A :: xorp (), а вызов b.xorp () фактически был бы вызовом bB :: xorp () .

... больше не будет истинным внутри baz.

...