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

Должен ли я использовать виртуальные функции Initialize() для инициализации объекта моего класса?

В настоящее время я беседую с моим учителем о дизайне классов, и мы подошли к Initialize() функциям, которые он активно продвигает. Пример:

class Foo{
public:
  Foo()
  { // acquire light-weight resources only / default initialize
  }

  virtual void Initialize()
  { // do allocation, acquire heavy-weight resources, load data from disk
  }

  // optionally provide a Destroy() function
  // virtual void Destroy(){ /*...*/ }
};

Все с необязательными параметрами, конечно.

Теперь он также делает упор на расширяемость и использование в иерархиях классов (он разработчик игр и его компания продает игровой движок) со следующими аргументами (взято дословно, только переведено):

Аргументы против конструкторов:

  • не может быть переопределен производными классами
  • не может вызывать виртуальные функции

Аргументы для функций Initialize():

  • производный класс может полностью заменить код инициализации
  • производный класс может инициализировать базовый класс в любое время во время его собственной инициализации

Мне всегда учили выполнять настоящую инициализацию непосредственно в конструкторе и не предоставлять такие функции Initialize(). Тем не менее, я уверен, что у меня не так много опыта, как он делает, когда дело доходит до развертывания библиотеки/движка, поэтому я подумал, что спрошу у хорошего "SO".

Итак, что же такое аргументы для таких функций Initialize() и против них? Это зависит от среды, где она должна использоваться? Если да, просьба указать аргументы для разработчиков библиотеки/движка или, если можно, даже разработчика игр в целом.


Изменить. Я должен был упомянуть, что такие классы будут использоваться как переменные-члены только в других классах, поскольку для них ничего не будет иметь смысла. К сожалению.

4b9b3361

Ответ 1

Для Initialize: именно то, что говорит ваш учитель, но в хорошо продуманном коде вам, вероятно, никогда не понадобится.

Против: нестандартного, может быть проигнорировано назначение конструктора при неправильном использовании. Что еще более важно: клиенту нужно запомнить вызов Initialize. Таким образом, каждый экземпляр будет находиться в противоречивом состоянии при построении, или им нужно много дополнительной бухгалтерии, чтобы клиентский код не вызывал ничего другого:

void Foo::im_a_method()
{
    if (!fully_initialized)
        throw Unitialized("Foo::im_a_method called before Initialize");
    // do actual work
}

Единственный способ предотвратить этот тип кода - начать использовать функции factory. Итак, если вы используете Initialize в каждом классе, вам понадобится factory для каждой иерархии.

Другими словами: не делайте этого, если это не необходимо; всегда проверяйте, может ли код быть переработан с точки зрения стандартных конструкций. И, конечно же, не добавляйте член public Destroy, это задача деструктора. Деструкторы могут (и в ситуациях наследования, должны) быть virtual в любом случае.

Ответ 2

Я против "двойной инициализации" на С++ вообще.

Аргументы против конструкторов:

  • не может быть переопределен производными классами
  • не может вызывать виртуальные функции

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

Derived::Derived() : Base(GetSomeParameter()) 
{
}

Ответ 3

Это ужасная, страшная идея. Спросите себя, какой смысл конструктора, если вам просто нужно позвонить Initialize() позже? Если производный класс хочет переопределить базовый класс, то не выводить.

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

Ответ 4

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

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

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

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

Ответ 5

Голос раздора здесь.

  • Возможно, вы работаете в среде, где у вас нет выбора, кроме как разделить конструкцию и инициализацию. Добро пожаловать в мой мир. Не говорите мне, чтобы найти другую среду; У меня нет выбора. Предпочтительный вариант осуществления продуктов, которые я создаю, не в моих руках.

  • Расскажите, как инициализировать некоторые аспекты объекта B по отношению к объекту C, другие аспекты относительно объекта A; некоторые аспекты объекта C относительно объекта B, другие аспекты относительно объекта A. В следующий раз ситуация может быть полностью отменена. Я даже не пойду, как инициализировать объект A. По-видимому, круговые зависимости инициализации могут быть разрешены, но не конструкторами.

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

Ответ 6

Забудьте о функции Initialize() - это задание конструктора.

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

Ответ 7

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

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

Ответ 8

Не вызывать Initialize может быть легко сделать случайно и не даст вам правильно построенный объект. Он также не соответствует принципу RAII, так как есть отдельные шаги в построении/разрушении объекта: что произойдет, если Initialize не удается (как вы имеете дело с недопустимым объектом)?

Вызывая инициализацию по умолчанию, вы можете больше работать, чем выполнять инициализацию в самом конструкторе.

Ответ 9

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

Ответ 10

Другие подробно рассуждали об использовании Initialize, я сам вижу одно использование: лень.

Например:

File file("/tmp/xxx");
foo(file);

Теперь, если foo никогда не использует file (в конце концов), то совершенно не нужно пытаться его читать (и это действительно было бы пустой тратой ресурсов).

В этой ситуации, я поддерживаю Lazy Initialization, однако, он не должен полагаться на клиента, вызывающего функцию, но, скорее, каждая функция-член должна проверять, необходимо ли инициализировать или нет. В этом примере name() не требует этого, но encoding() делает.

Ответ 11

Используйте только функцию инициализации, если у вас нет данных, доступных в момент создания.

Например, вы динамически создаете модель данных, а данные, определяющие иерархию объектов, должны потребляться перед данными, описывающими параметры объекта.

Ответ 12

Если вы его используете, вы должны сделать конструктор закрытым и использовать методы factory вместо этого, чтобы вызвать метод initialize() для вас. Например:

class MyClass
{
public:
    static std::unique_ptr<MyClass> Create()
    {
        std::unique_ptr<MyClass> result(new MyClass);
        result->initialize();
        return result;
    }

private:
    MyClass();

    void initialize();
};

Тем не менее, методы инициализации не очень элегантны, но они могут быть полезны по тем причинам, которые сказал ваш учитель. Я бы не считал их "неправильными" как таковыми. Если ваш дизайн хорош, вы, вероятно, никогда им не понадобятся. Однако реальный код иногда заставляет вас пойти на компромиссы.

Ответ 13

Некоторые члены просто должны иметь значения при построении (например, ссылки, значения const, объекты, предназначенные для RAII без конструкторов по умолчанию)... они не могут быть построены в функции initialise(), а некоторые не могут быть переназначается.

Итак, в общем случае это не выбор конструктора vs. initialise(), это вопрос о том, закончится ли ваш код разделенным между ними.

Из баз и элементов, которые могут быть инициализированы позже, для производного класса это означает, что они не private; если вы заходите так, чтобы сделать базы/члены не private ради задержки инициализации, вы нарушаете инкапсуляцию - один из основных принципов ООП. Прерывание инкапсуляции препятствует разработчикам (разработчикам) базового класса рассуждать об инвариантах, которые должен защитить класс; они не могут разработать свой код, не рискуя сломать производные классы, что может не иметь видимости.

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

  • [конструкторы] не могут быть переопределены производными классами

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

  • [конструкторы] не могут вызывать виртуальные функции

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

Аргументы для функций Initialize():

  • производный класс может полностью заменить код инициализации

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

  • производный класс может инициализировать базовый класс в любое время во время его собственной инициализации

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

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

Если initialise() нужно вызвать, чтобы установить указатель на nullptr или значение, безопасное для деструктора delete, но некоторые другие данные или код сначала бросаются, все ад разрывается.

initialise() также заставляет весь класс быть не const в клиентском коде, даже если клиент просто хочет создать начальное состояние и убедиться, что он не будет дополнительно изменен - ​​в основном вы выбросили const -коррекция в окне.

Код, делающий такие вещи, как p_x = new X(values, for, initialisation);, f(X(values, for initialisation), v.push_back(X(values, for initialisation)), не будет возможен - форсирование подробных и неуклюжих альтернатив.

Если используется функция destroy(), многие из вышеперечисленных проблем усугубляются.