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

Зачем использовать метод инициализации вместо конструктора?

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

struct MyFancyClass : theUberClass
{
    MyFancyClass();
    ~MyFancyClass();
    resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
                                redundantArgument arg3=TODO);
    // several fancy methods...
};

Они сказали мне, что это имеет какое-то отношение к срокам. Что-то должно быть сделано после построения, которое потерпит неудачу в конструкторе. Но большинство конструкторов пустые, и я не вижу причин для отказа от использования конструкторов.

Итак, я обращаюсь к вам, о волшебники С++: зачем вы используете init-метод вместо конструктора?

4b9b3361

Ответ 1

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

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

Ответ 2

Да, я могу думать о нескольких, но в целом это не очень хорошая идея.

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

Однако в правильно спроектированном OO-коде конструктор отвечает за установление инвариантов класса. Предоставляя конструктор по умолчанию, вы разрешаете пустой класс, поэтому вам нужно изменить инварианты, чтобы он принимал как "нулевой" класс, так и "осмысленный" класс... и каждое использование класса должно сначала обеспечить, чтобы объект была правильно построена... это грубо.

Итак, позвольте развенчать "причины":

  • Мне нужно использовать метод virtual: используйте идиому Virtual Constructor.
  • Должна быть выполнена большая работа: так что, работа будет выполнена в любом случае, просто сделайте это в конструкторе
  • Настройка может завершиться неудачно: выбросить исключение
  • Я хочу сохранить частично инициализированный объект: использовать try/catch внутри конструктора и установить причину ошибки в поле объекта, не забывайте assert в начале каждого публичного метода, чтобы убедиться, что объект можно использовать, прежде чем пытаться использовать его.
  • Я хочу повторно инициализировать свой объект: вызывать метод инициализации из конструктора, вы избежите дублирования кода, все еще имея полностью инициализированный объект
  • Я хочу повторно инициализировать свой объект (2): используйте operator= (и реализуйте его с помощью идиомы копирования и подкачки, если сгенерированная версия компилятора не подходит вам).

Как сказано, в общем, плохая идея. Если вы действительно хотите иметь конструктор "void", сделайте их private и используйте методы Builder. Он эффективен с NRVO... и вы можете вернуть boost::optional<FancyObject> в случае неудачной сборки.

Ответ 3

Другие указали много возможных причин (и правильное объяснение того, почему большинство из них, как правило, не очень хорошая идея). Позвольте мне опубликовать один пример a (более или менее) действительного использования методов init, который фактически имеет отношение к хронологии.

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

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

Ответ 4

Две причины, которые я могу придумать с головы:

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

Ответ 5

Еще одно использование такой инициализации может быть в пуле объектов. В основном вы просто запрашиваете объект из пула. Пул будет иметь уже некоторые N объектов, которые пусты. Это вызывающий абонент, который может вызывать любой метод, который ему нравится устанавливать. Как только вызывающий абонент совершит операцию с объектом, он скажет, что пул может ее уничтожить. Преимущество заключается в том, что до тех пор, пока объект не будет использован, память будет сохранена, и вызывающий может использовать его собственный подходящий метод-член инициализации объекта. Объект может обслуживать множество целей, но вызывающему может не понадобиться все, а также может не понадобиться инициализировать весь член объектов.

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

Ответ 6

Функция

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

Подпрограммы

init() также полезны, когда необходимо определить порядок построения. То есть, если вы глобально выделяете объекты, порядок, в котором вызывается конструктор, не определен. Например:

[file1.cpp]
some_class instance1; //global instance

[file2.cpp]
other_class must_construct_before_instance1; //global instance

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

Ответ 7

И также мне нравится прикреплять образец кода, чтобы ответить # 1 -

Так как и msdn говорит:

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

Пример: Следующий пример демонстрирует эффект нарушения этого правила. Приложение-тест создает экземпляр DerivedType, который вызывает его конструктор базового класса (BadlyConstructedType). Конструктор BadlyConstructedType неправильно вызывает виртуальный метод DoSomething. Как показывает результат, DerivedType.DoSomething() выполняет и делает это до выполнения конструктора DerivedType.

using System;

namespace UsageLibrary
{
    public class BadlyConstructedType
    {
        protected  string initialized = "No";

        public BadlyConstructedType()
        {
            Console.WriteLine("Calling base ctor.");
            // Violates rule: DoNotCallOverridableMethodsInConstructors.
            DoSomething();
        }
        // This will be overridden in the derived type.
        public virtual void DoSomething()
        {
            Console.WriteLine ("Base DoSomething");
        }
    }

    public class DerivedType : BadlyConstructedType
    {
        public DerivedType ()
        {
            Console.WriteLine("Calling derived ctor.");
            initialized = "Yes";
        }
        public override void DoSomething()
        {
            Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized);
        }
    }

    public class TestBadlyConstructedType
    {
        public static void Main()
        {
            DerivedType derivedInstance = new DerivedType();
        }
    }
}

Выход:

Вызов базы ctor.

Производится DoSomething называется - инициализировано? Нет

Вызов производного ctor.

Ответ 8

Больше специального случая: если вы создаете прослушиватель, вы можете захотеть его зарегистрировать где-нибудь (например, с помощью singleton или GUI). Если вы делаете это во время своего конструктора, он теряет указатель/ссылку на себя, которая еще не безопасна, поскольку конструктор не завершил (и может даже полностью не работать). Предположим, что синглтон, который собирает всех слушателей и отправляет им события, когда что-то происходит, принимает событие и событие, а затем перебирает список своих слушателей (один из них - это тот экземпляр, о котором мы говорим), чтобы отправить их каждому сообщению. Но этот экземпляр по-прежнему находится в середине своего конструктора, поэтому вызов может терпеть неудачу во всех возможных случаях. В этом случае имеет смысл регистрироваться в отдельной функции, которую вы, очевидно, не вызываете от самого конструктора (который полностью победил бы цель), а из родительского объекта после завершения построения.

Но это конкретный случай, а не общий.

Ответ 9

Это полезно для управления ресурсами. Предположим, что у вас есть классы с деструкторами для автоматического освобождения ресурсов, когда срок жизни объекта завершен. Скажем, у вас также есть класс, который содержит эти классы ресурсов, и вы инициируете их в конструкторе этого верхнего класса. Что произойдет, если вы используете оператор присваивания для запуска этого более высокого класса? После копирования содержимого старый более высокий класс выходит из контекста, а деструкторы вызываются для всех классов ресурсов. Если эти классы ресурсов имеют указатели, которые были скопированы во время назначения, то все эти указатели теперь являются плохими указателями. Если вы вместо этого инициируете классы ресурсов в отдельной функции init в более высоком классе, вы полностью обходите деструктор класса ресурсов из когда-либо вызываемого, потому что оператор присваивания никогда не должен создавать и удалять эти классы. Я считаю, что это то, что подразумевалось под требованием "времени".

Ответ 10

Еще несколько случаев:

ПРИГОТОВЛЕНИЕ АРГОВ

Конструктор не может вызвать другой конструктор, но метод init может вызвать другой init.

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

Это хорошо, когда инициализаторы являются методами init, но не когда инициализаторы являются конструкторами.

Цыпленок или яйцо

У нас может быть класс автомобиля, инициализатор которого должен иметь указатель на объект двигателя, а инициализатор класса двигателя должен иметь указатель на его объект автомобиля. Это просто невозможно с конструкторами, но тривиально с методами init.

РАЗРЫВАЯ СПИСОК ARG

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

Опять же, просто невозможно разбить конструктор, но тривиально разбить инициализатор.

Ответ 11

Вы используете метод инициализации вместо конструктора, если инициатор должен быть вызван ПОСЛЕ того, как класс был создан. Поэтому, если класс A был создан как:

A *a = new A;

и для инициализатора класса A требуется, чтобы a было установлено, то, очевидно, вам нужно что-то вроде:

A *a = new A;
a->init();