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

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

16 ответов

Решение

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

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

Выдержка:

[...] В конструкторе механизм виртуального вызова отключен, потому что переопределение из производных классов еще не произошло. Объекты строятся от основания до "от основания до производного".

[...]

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

РЕДАКТИРОВАТЬ Исправлено больше всего для всех (спасибо)

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

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

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

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 компилятор будет создавать виртуальную эквивалентную таблицу на самом первом этапе построения, прежде чем вводить конструктор Base или конструктор Derived. Последствия разные (и на мой взгляд более опасные). Если конструктор базового класса вызывает метод, который переопределяется в производном классе, вызов будет фактически обработан на производном уровне, вызывая метод для неструктурированного объекта, что приведет к неожиданным результатам. Все атрибуты производного класса, которые инициализируются внутри блока конструктора, еще не инициализированы, включая атрибуты 'final'. Элементы, которые имеют значение по умолчанию, определенное на уровне класса, будут иметь это значение.

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++ создаются как лук изнутри. Суперклассы создаются перед производными классами. Таким образом, прежде чем можно будет сделать B, нужно сделать A. Когда вызывается конструктор A, это еще не B, поэтому в таблице виртуальных функций все еще есть запись для копии A функции fn().

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

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

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

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

  return pObject;
}
  • Используйте это так:
класс MyClass: публичный объект 
{
общественности:
  виртуальная пустота afterConstruction()
  {
    // сделай что-нибудь.
  }
  //...
};

MyClass * pMyObject = factoryNew ();

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

Добавив конструктор шаблона к базовому типу так, чтобы аргумент шаблона всегда выводился как производный тип, можно узнать конкретный тип производного типа. Оттуда вы можете позвонить static функции-члены для этого производного типа.

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

Вот пример реализации:

#include <iostream>
#include <string>

struct Base {
protected:
    template<class T>
    explicit Base(const T*) : class_name(T::Name())
    {
        std::cout << class_name << " created\n";
    }

public:
    Base() : class_name(Name())
    {
        std::cout << class_name << " created\n";
    }


    virtual ~Base() {
        std::cout << class_name << " destroyed\n";
    }

    static std::string Name() {
        return "Base";
    }

private:
    std::string class_name;
};


struct Derived : public Base
{   
    Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T

    static std::string Name() {
        return "Derived";
    }
};

int main(int argc, const char *argv[]) {

    Derived{};  // Create and destroy a Derived
    Base{};     // Create and destroy a Base

    return 0;
}

Этот пример должен напечатать

Derived created
Derived destroyed
Base created
Base destroyed

Когда Derived построен, Base Поведение конструктора зависит от фактического динамического типа конструируемого объекта.

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

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

Стандарт C++ (ISO/IEC 14882-2014) гласит:

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

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

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

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

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

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

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

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

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

Например:

      #include <thread>
#include <string>
#include <iostream>
#include <chrono>

class Base
{
public:
  Base()
  {
    std::thread worker([this] {
      // This will print "Base" rather than "Sub".
      this->Print();
    });
    worker.detach();
    // Try comment out this code to see different output.
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  virtual void Print()
  {
    std::cout << "Base" << std::endl;
  }
};

class Sub : public Base
{
public:
  void Print() override
  {
    std::cout << "Sub" << std::endl;
  }
};

int main()
{
  Sub sub;
  sub.Print();
  getchar();
  return 0;
}

Это выведет:

      Base
Sub

Чтобы ответить, что происходит / почему, когда вы запускаете этот код, я скомпилировал его через g++ -ggdb main.cc, и перешагнул через gdb.

main.cc:

      class A { 
  public:
    A() {
      fn();
    }
    virtual void fn() { _n=1; }
    int getn() { return _n; }

  protected:
    int _n;
};


class B: public A {
  public:
    B() {
      // fn();
    }
    void fn() override {
      _n = 2;
    }
};


int main() {
  B b;
}

Установка точки останова на main, затем перейдем в B (), распечатаем ptr, сделаем шаг в A ()(базовый конструктор):

      (gdb) step
B::B (this=0x7fffffffde80) at main2.cc:16
16    B() {
(gdb) p this
$27 = (B * const) 0x7fffffffde80
(gdb) p *this
$28 = {<A> = {_vptr.A = 0x7fffffffdf80, _n = 0}, <No data fields>}
(gdb) s
A::A (this=0x7fffffffde80) at main2.cc:3
3     A() {
(gdb) p this
$29 = (A * const) 0x7fffffffde80

показывает, что изначально указывает на производную B obj bстроится в стеке по адресу 0x7fffffffde80. Следующий шаг находится в базовом A () ctor и становится A * constпо тому же адресу, что имеет смысл, поскольку база A находится прямо в начале объекта B. но он до сих пор не построен:

      (gdb) p *this
$30 = {_vptr.A = 0x7fffffffdf80, _n = 0}

Еще один шаг:

      (gdb) s
4       fn();
(gdb) p *this
$31 = {_vptr.A = 0x402038 <vtable for A+16>, _n = 0}

_n был инициализирован, и указатель на его таблицу виртуальных функций содержит адрес virtual void A::fn():

      (gdb) p fn
$32 = {void (A * const)} 0x40114a <A::fn()>
(gdb) x/1a 0x402038
0x402038 <_ZTV1A+16>:   0x40114a <_ZN1A2fnEv>

Таким образом, имеет смысл, что следующий шаг выполняет A::fn() через this-> fn() с учетом активных и. Еще один шаг, и мы снова в B() ctor:

      (gdb) s
B::B (this=0x7fffffffde80) at main2.cc:18
18    }
(gdb) p this
$34 = (B * const) 0x7fffffffde80
(gdb) p *this
$35 = {<A> = {_vptr.A = 0x402020 <vtable for B+16>, _n = 1}, <No data     fields>}

База A построена. Обратите внимание, что адрес, хранящийся в указателе таблицы виртуальных функций, изменился на vtable для производного класса B. И поэтому вызов fn() выберет переопределение производного класса B :: fn() через this-> fn() с учетом активного this и (вызов без комментариев к B :: fn() в B (), чтобы увидеть это.) Снова проверка 1 адреса, хранящегося в _vptr.A, показывает, что теперь он указывает на переопределение производного класса:

      (gdb) p fn
$36 = {void (B * const)} 0x401188 <B::fn()>
(gdb) x/1a 0x402020
0x402020 <_ZTV1B+16>:   0x401188 <_ZN1B2fnEv>

Глядя на этот пример и на пример с трехуровневым наследованием, кажется, что по мере того, как компилятор спускается для создания базовых подобъектов, тип this* и соответствующий адрес в _vptr.Aизменить, чтобы отразить текущий создаваемый подобъект, - чтобы он оставался указывающим на наиболее производный тип. Таким образом, мы ожидаем, что виртуальные функции, вызываемые из ctors, выберут функцию для этого уровня, то есть с таким же результатом, как если бы они были не виртуальными .. То же самое для dtors, но в обратном порядке.

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

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

У меня просто была эта ошибка в программе. И я подумал: что произойдет, если метод помечен как чисто виртуальный в конструкторе?

      class Base {
public:
    virtual int getInt() = 0;
    
    Base(){
        printf("int=%d\n", getInt());
    }
};

class Derived : public Base {
    public:
        virtual int getInt() override {return 1;}
};

И... забавная вещь! Сначала вы получаете предупреждение от компилятора:

      warning: pure virtual ‘virtual int Base::getInt() const’ called from constructor

И ошибка от ld!

      /usr/bin/ld: /tmp/ccsaJnuH.o: in function `Base::Base()':
main.cpp:(.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]+0x26): undefined reference to `Base::getInt()'
collect2: error: ld returned 1 exit status

Совершенно нелогично, что вы получаете просто предупреждение от компилятора!

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

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

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