Вызов виртуальных функций внутри конструкторов - PullRequest
211 голосов
/ 07 июня 2009

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

Ответы [ 13 ]

195 голосов
/ 07 июня 2009

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

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

Выдержка:

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

[...]

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

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

78 голосов
/ 07 июня 2009

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

54 голосов
/ 07 июня 2009

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

23 голосов
/ 07 июня 2009

Часто задаваемые вопросы по C ++ Lite Охватывает это довольно хорошо:

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

13 голосов
/ 07 июня 2009

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

  • Определите общий базовый класс для вашей иерархии классов, содержащий виртуальный метод 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();

1 голос
/ 19 октября 2018

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

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

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

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

#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 зависит от фактического динамического типа создаваемого объекта.

1 голос
/ 23 августа 2017

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

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

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

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

1 голос
/ 07 мая 2013

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

1 голос
/ 07 июня 2009

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

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

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

0 голосов
/ 11 апреля 2018

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

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

...