Зачем использовать метод инициализации вместо конструктора? - PullRequest
46 голосов
/ 24 сентября 2010

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

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

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

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

Ответы [ 10 ]

65 голосов
/ 24 сентября 2010

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

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

28 голосов
/ 24 сентября 2010

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

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

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

Итак, давайте разберемся с «причинами»:

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

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

16 голосов
/ 24 сентября 2010

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

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

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

7 голосов
/ 24 сентября 2010

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

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

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

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

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

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

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

5 голосов
/ 24 сентября 2010

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

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

1 голос
/ 24 июля 2012

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

1 голос
/ 24 сентября 2010

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

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

1 голос
/ 24 сентября 2010

А также мне нравится прикреплять пример кода к ответу # 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.

0 голосов
/ 24 сентября 2010

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

A *a = new A;

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

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