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

Вызов виртуального метода в конструкторе базового класса

Я знаю, что вызов виртуального метода из конструктора базового класса может быть опасным, поскольку дочерний класс может не находиться в допустимом состоянии. (по крайней мере, на С#)

Мой вопрос: что, если виртуальным методом является тот, кто инициализирует состояние объекта? Это хорошая практика или это должен быть двухэтапный процесс, сначала создать объект, а затем загрузить состояние?

Первая опция: (используя конструктор для инициализации состояния)

public class BaseObject {
    public BaseObject(XElement definition) {
        this.LoadState(definition);
    }

    protected abstract LoadState(XElement definition);
}

Второй вариант: (используя двухэтапный процесс)

public class BaseObject {
    public void LoadState(XElement definition) {
        this.LoadStateCore(definition);
    }

    protected abstract LoadStateCore(XElement definition);
}

В первом методе потребитель кода может создавать и инициализировать объект с помощью одного оператора:

// The base class will call the virtual method to load the state.
ChildObject o = new ChildObject(definition)

Во втором методе потребитель должен будет создать объект, а затем загрузить состояние:

ChildObject o = new ChildObject();
o.LoadState(definition);
4b9b3361

Ответ 1

(Этот ответ относится к С# и Java. Я считаю, что С++ по-другому работает по этому вопросу.)

Вызов виртуального метода в конструкторе действительно опасен, но иногда он может содержать самый чистый код.

Я постараюсь избежать этого, где это возможно, но не сильно изгибая дизайн. (Например, опция "initialize later" запрещает неизменность.) Если вы используете виртуальный метод в конструкторе, очень сильно документируйте его. Пока все участвующие знают о том, что он делает, это не должно вызывать слишком много проблем. Однако я попытался бы ограничить видимость, как это было в первом примере.

EDIT: Важно то, что здесь важна разница между С# и Java в порядке инициализации. Если у вас есть класс, например:

public class Child : Parent
{
    private int foo = 10;

    protected override void ShowFoo()
    {
        Console.WriteLine(foo);
    }
}

где конструктор Parent вызывает ShowFoo, в С# он отобразит 10. Эквивалентная программа в Java отобразит 0.

Ответ 2

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

Протестировано с помощью GCC, оно позволяет вызывать чистую виртуальную функцию от конструктора, но дает предупреждение и приводит к ошибке времени соединения. Похоже, что это поведение undefined по стандарту:

"Функции-члены могут быть вызваны из конструктора (или деструктора) абстрактного класса; эффект виртуального вызова (class.virtual) на чистую виртуальную функцию прямо или косвенно для создаваемого (или уничтоженного) объекта от такого конструктора (или деструктора) undefined."

Ответ 3

С С++ виртуальные методы маршрутизируются через vtable для создаваемого класса. Таким образом, в вашем примере он генерирует чистое исключение виртуального метода, поскольку в то время как BaseObject создается, просто не существует метода LoadStateCore для вызова.

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

По этой причине вы просто не можете сделать это в С++...

Ответ 4

Для С++ базовый конструктор вызывается перед производным конструктором, что означает, что виртуальная таблица (которая содержит адреса переопределенных виртуальных функций производного класса) еще не существует. По этой причине считается ОЧЕНЬ опасной задачей (особенно если функции являются чисто виртуальными в базовом классе... это вызовет чисто-виртуальное исключение).

Существует два способа:

  • Сделайте двухэтапный процесс построения + инициализация
  • Переместите виртуальные функции во внутренний класс, который вы можете более тщательно контролировать (можете использовать приведенный выше подход, см. пример для деталей)

Примером (1) является:

class base
{
public:
    base()
    {
      // only initialize base members
    }

    virtual ~base()
    {
      // only release base members
    }

    virtual bool initialize(/* whatever goes here */) = 0;
};

class derived : public base
{
public:
    derived ()
    {
      // only initialize derived  members
    }

    virtual ~derived ()
    {
      // only release derived  members
    }

    virtual bool initialize(/* whatever goes here */)
    {
      // do your further initialization here
      // return success/failure
    }
};

Примером (2) является:

class accessible
{
private:
    class accessible_impl
    {
    protected:
        accessible_impl()
        {
          // only initialize accessible_impl members
        }

    public:
        static accessible_impl* create_impl(/* params for this factory func */);

        virtual ~accessible_impl()
        {
          // only release accessible_impl members
        }

        virtual bool initialize(/* whatever goes here */) = 0;
    };

    accessible_impl* m_impl;

public:
    accessible()
    {
        m_impl = accessible_impl::create_impl(/* params to determine the exact type needed */);

        if (m_impl)
        {
            m_impl->initialize(/* ... */);  // add any initialization checking you need
        }
    }

    virtual ~accessible()
    {
        if (m_impl)
        {
            delete m_impl;
        }
    }

    /* Other functionality of accessible, which may or may not use the impl class */
};

Подход (2) использует шаблон Factory, чтобы обеспечить соответствующую реализацию для класса accessible (который предоставит тот же интерфейс, что и ваш класс base). Одним из основных преимуществ здесь является то, что вы получаете инициализацию при построении accessible, которая может безопасно использовать виртуальные элементы accessible_impl.

Ответ 5

В С++, раздел 12.7, пункт 3 Стандарта охватывает этот случай.

Подводя итог, это законно. Он решит правильную функцию на тип выполняемого конструктора. Поэтому, адаптировав свой пример к синтаксису С++, вы вызываете BaseObject::LoadState(). Вы не можете добраться до ChildObject::LoadState() и попытаться сделать это, указав класс, а также результаты функции в undefined.

Конструкторы абстрактных классов описаны в разделе 10.4, параграф 6. Вкратце, они могут вызывать функции-члены, но вызов чистой виртуальной функции в конструкторе - это поведение undefined. Не делайте этого.

Ответ 6

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

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

то есть.

public class BaseClass
{
    public BaseClass(XElement defintion)
    {
        // base class loads state here
    }
}

public class DerivedClass : BaseClass
{
    public DerivedClass (XElement defintion)
        : base(definition)
    {
        // derived class loads state here
    }
}

Тогда ваш код очень прост, и у вас нет проблем с вызовом виртуального метода.

Ответ 7

Для С++ прочитайте статью Скотта Мейера:

Никогда не вызывать виртуальные функции во время строительства или уничтожения

ps: обратите внимание на это исключение в статье:

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

Ответ 8

Обычно вы можете обойти эти проблемы, имея более жадный конструктор базы. В вашем примере вы передаете XElement в LoadState. Если вы разрешаете прямое задание в вашем базовом конструкторе, тогда ваш дочерний класс может анализировать XElement перед вызовом вашего конструктора.

public abstract class BaseObject {
   public BaseObject(int state1, string state2, /* blah, blah */) {
      this.State1 = state1;
      this.State2 = state2;
      /* blah, blah */
   }
}

public class ChildObject : BaseObject {
   public ChildObject(XElement definition) : 
      base(int.Parse(definition["state1"]), definition["state2"], /* blah, blah */) {
   }
}

Если дочерний класс должен выполнять хороший бит работы, он может отключить статический метод.

Ответ 9

В С++ совершенно безопасно вызывать виртуальные функции из базового класса - пока они не чисты - с некоторыми ограничениями. Однако вы не должны этого делать. Лучше инициализировать объекты, используя не виртуальные функции, которые явно помечены как такие функции инициализации, используя комментарии и соответствующее имя (например, initialize). Если он даже объявлен чистым виртуальным в классе, вызывающем его, поведение undefined.

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

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

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

struct base {
    base() { init(); }
    virtual void init() = 0;
};

struct derived : base {
    derived() {
        // we would expect str to be "called it", but actually the
        // default constructor of it initialized it to an empty string
    }
    virtual void init() {
        // note, str not yet constructed, but we can't know this, because
        // we could have called from derived constructors body too
        str = "called it";
    }
private:
    string str;
};

Эта проблема действительно может быть решена путем изменения Стандарта С++ и ее разрешения - настройка определения конструкторов, времени жизни объекта и много чего. Правила должны быть сделаны, чтобы определить, что означает str = ...; для еще не построенного объекта. И обратите внимание, как эффект от этого зависит от того, кто вызвал init. Функция, которую мы получаем, не оправдывает проблем, которые мы должны решить тогда. Таким образом, С++ просто запрещает динамическую отправку во время создания объекта.