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

32
задан tereško 12 July 2014 в 07:49
поделиться

9 ответов

(Этот ответ относится к 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.

39
ответ дан 27 November 2019 в 20:25
поделиться

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

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

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

10
ответ дан 27 November 2019 в 20:25
поделиться

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

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

Поэтому Вы просто не можете сделать этого этот путь в C++...

4
ответ дан 27 November 2019 в 20:25
поделиться

Для 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 */
};

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

4
ответ дан 27 November 2019 в 20:25
поделиться

Для C++ разделите 12.7, абзац 3 Стандарта касается этого случая.

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

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

3
ответ дан 27 November 2019 в 20:25
поделиться

Если у Вас есть класс как показано в Вашем сообщении, которое берет 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
ответ дан 27 November 2019 в 20:25
поделиться

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

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

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

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

3
ответ дан 27 November 2019 в 20:25
поделиться

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

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

1
ответ дан 27 November 2019 в 20:25
поделиться

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

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

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

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

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
ответ дан 27 November 2019 в 20:25
поделиться
Другие вопросы по тегам:

Похожие вопросы: