Вызов виртуальных функций внутри конструкторов

Предположим, у меня есть два класса C++:

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

если я напишу следующий код:

int main()
{
  B b;
  int n = b.getn();
}

можно было бы ожидать, что n имеет значение 2.

получается, что n имеет значение 1. Почему?

12 ответов


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

на C++ FAQ Lite охватывает это в разделе 23.7 довольно подробно. Я предлагаю прочитать это (и остальную часть FAQ) для последующего просмотра.

редактировать исправлено больше всего (спасибо litb)


вызов полиморфной функции из конструктора является рецептом катастрофы в большинстве языков OO. При возникновении такой ситуации разные языки будут работать по-разному.

основная проблема заключается в том, что во всех языках базовый тип(Ы) должен быть построен до производного типа. Теперь проблема в том, что означает вызов полиморфного метода из конструктора. На что ты рассчитываешь? Существует два подхода: вызов метода в базе уровень (стиль C++) или вызов полиморфного метода на неструктурированном объекте в нижней части иерархии (путь Java).

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

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

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

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

Как вы видите, вызов полиморфного (виртуальный в терминологии C++) методы являются общими источник ошибки. В C++, по крайней мере, у вас есть гарантия, что он никогда не вызовет метод для еще неструктурированного объекта...


причина в том, что объекты C++ построены как лук, изнутри наружу. Супер-классы строятся перед производными классами. Итак, прежде чем сделать B, нужно сделать A. Когда вызывается конструктор A, это еще не B, поэтому в таблице виртуальных функций все еще есть запись для копии FN () A.


на C++ FAQ Lite охватывает это очень хорошо:

по существу, во время вызова конструктора базовых классов объект еще не является производным типом, и, таким образом, реализация виртуального типа базового типа вызывается, а не производного типа.


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

  • определите общий базовый класс для иерархии классов, содержащий виртуальный метод afterConstruction ():
class Object
{
public:
  virtual void afterConstruction() {}
  // ...
};
  • определите заводской метод:
template< class C >
C* factoryNew()
{
  C* pObject = new C();
  pObject->afterConstruction();

  return pObject;
}
  • используйте его так:
class MyClass : public Object 
{
public:
  virtual void afterConstruction()
  {
    // do something.
  }
  // ...
};

MyClass* pMyObject = factoryNew();


знаете ли вы ошибку аварии из Проводника Windows?! " чисто виртуальный вызов функции ..."
та же проблема ...

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

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


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


на стандарт C++ (ISO/IEC 14882-2014) говорят:

функции-члены, включая виртуальные функции (10.3), можно вызвать во время строительства или уничтожения (12.6.2). Когда виртуальная функция вызывается прямо или косвенно из конструктора или из деструктор, в том числе при строительстве или разрушении нестатические члены данных класса и объект, к которому вызывается применяется объект (назовите его x) в стадии строительства или разрушение, функция называется конечный overrider в конструкторе или класс деструктора, а не один, переопределяющий его в более производном классе. Если вызов виртуальной функции использует явный доступ к члену класса (5.2.5) и выражение объекта относится к полному объекту x или один из субобъектов базового класса этого объекта, но не x или один из его субобъекты базового класса, поведение undefined.

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

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


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

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

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

однако это можно решить с помощью полиморфных геттеров, которые используют статический полиморфизм вместо виртуальных функций, если ваши геттеры возвращают константы или иначе могут быть выражены в статической функции-члене, в этом примере используется CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

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

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}

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


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


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