Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

Ответ 4

Часто задаваемые вопросы C++ Lite:

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

Ответ 5

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

  • Определите общий базовый класс для вашей иерархии классов, содержащий виртуальный метод afterConstruction():
class Object
{
public:
  virtual void afterConstruction() {}
  // ...
};
  • Определите метод factory:
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();

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

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

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

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

Ответ 9

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

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

Ответ 10

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

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

Ответ 11

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

Ответ 12

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