Вызов виртуального метода в конструкторе базового класса - PullRequest
32 голосов
/ 15 января 2009

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

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

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

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);

Ответы [ 9 ]

39 голосов
/ 15 января 2009

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

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

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

РЕДАКТИРОВАТЬ: Здесь важно то, что между C # и Java существует различие в порядке инициализации. Если у вас есть такой класс, как:

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

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

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

10 голосов
/ 15 января 2009

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

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

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

4 голосов
/ 16 января 2009

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

Есть два способа обойти это:

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

Пример (1):

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

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

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

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

    virtual ~derived ()
    {
      // only release derived 's 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's members
        }

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

        virtual ~accessible_impl()
        {
          // only release accessible_impl's 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 */
};

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

4 голосов
/ 15 января 2009

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

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

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

3 голосов
/ 16 января 2009

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

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

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

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

3 голосов
/ 16 января 2009

Если у вас есть класс, как показано в вашем посте, который занимает 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
    }
}

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

3 голосов
/ 15 января 2009

Для C ++, раздел 12.7, параграф 3 Стандарта охватывает этот случай.

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

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

1 голос
/ 16 января 2009

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

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

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

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

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's constructors body too
        str = "called it";
    }
private:
    string str;
};

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

1 голос
/ 15 января 2009

Обычно вы можете обойти эти проблемы, имея более жадный базовый конструктор. В вашем примере вы передаете 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 */) {
   }
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...