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



Предположим, у меня есть два класса 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. Зачем?

657   12  

12 ответов:

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

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

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

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

В 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, поэтому в таблице виртуальных функций все еще есть запись для копии a FN().

The 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;
};

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

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

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

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

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

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

сначала создается объект, а затем мы присваиваем его адрес указателям.Конструкторы вызываются во время создания объекта и используются для инициализации значения элементов данных. Указатель на объект входит в сценарий после создания объекта. Вот почему, 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++.

Comments

    Ничего не найдено.