Вопрос

Какой хороший существующий класс / шаблон проектирования для многоступенчатого построения / инициализации объекта в C ++?

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

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

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

Это было полезно?

Решение

Ключевой вопрос заключается в том, следует ли вам отличать полностью заполненные объекты от не полностью заполненных объектов на уровне типа.Если вы решите не проводить различия, то просто используйте boost::optional или что-то подобное, что вы делаете:это упрощает быстрое освоение программирования.OTOH вы не можете заставить компилятор применять требование о том, что для конкретной функции требуется полностью заполненный объект;вам необходимо каждый раз выполнять проверку полей во время выполнения.

Типы групп параметров

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

Типы "Частичных объектов"

Это прекрасно работает, если вам на самом деле не нужно делай что-нибудь с объектом до того, как он будет полностью заполнен (возможно, кроме таких тривиальных вещей, как возврат значений полей).Если вам иногда приходится обрабатывать не полностью заполненный X как "полный" X, вы можете вместо этого сделать X производный от типа XPartial, который содержит всю логику, плюс protected виртуальные методы для выполнения предварительных тестов, которые проверяют, заполнены ли все необходимые поля.Тогда , если X гарантирует, что он может быть создан только в полностью заполненном состоянии, он может переопределять эти защищенные методы с помощью тривиальных проверок, которые всегда возвращают true:

class XPartial {
    optional<string> name_;

public:
    void setName(string x) { name_.reset(x); }  // Can add getters and/or ctors
    string makeGreeting(string title) {
        if (checkMakeGreeting_()) {             // Is it safe?
            return string("Hello, ") + title + " " + *name_;
        } else {
            throw domain_error("ZOINKS");       // Or similar
        }
    }
    bool isComplete() const { return checkMakeGreeting_(); }  // All tests here

protected:
    virtual bool checkMakeGreeting_() const { return name_; }   // Populated?
};

class X : public XPartial {
    X();     // Forbid default-construction; or, you could supply a "full" ctor

public:
    explicit X(XPartial const& x) : XPartial(x) {  // Avoid implicit conversion
        if (!x.isComplete()) throw domain_error("ZOINKS");
    }

    X& operator=(XPartial const& x) {
        if (!x.isComplete()) throw domain_error("ZOINKS");
        return static_cast<X&>(XPartial::operator=(x));
    }

protected:
    virtual bool checkMakeGreeting_() { return true; }   // No checking needed!
};

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

Первоначально у меня было isComplete() как protected, но обнаружил , что это не сработало с тех пор , как Xctor копирования и оператор присваивания должны вызывать эту функцию на своих XPartial& аргумент, и у них нет достаточного доступа.Поразмыслив, имеет больше смысла публично раскрыть эту функциональность.

Другие советы

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

struct Big {
    char a[1000000];
};

class A {
  public: 
    A() : big(0) {}
   ~A() { delete big; }

   void f() {
      makebig();
      big->a[42] = 66;
   }
  private:
    Big * big;
    void makebig() {
      if ( ! big ) {
         big = new Big;
      }
    }
};

Я не вижу необходимости в чем-то более причудливом, за исключением того, что makebig(), вероятно, должен быть const (и, возможно, встроенным), а Большой указатель, вероятно, должен быть изменяемым.И, конечно, A должен иметь возможность создавать Big , что в других случаях может означать кэширование параметров конструктора содержащегося класса.Вам также нужно будет определиться с политикой копирования / присвоения - я бы, вероятно, запретил и то, и другое для такого рода классов.

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

Я бы использовал указатели для этих элементов, и когда их нужно будет сконструировать, выделял бы их одновременно.Вы можете использовать auto_ptr для них и проверить значение NULL, чтобы увидеть, инициализированы ли они.(Я думаю, что указатели - это встроенный "необязательный" тип в C / C ++ / Java, есть и другие языки, где NULL не является допустимым указателем).

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

image = new Image("unicorn.jpeg"); /* I'm not fond of this style */

или, я мог бы сделать это:

image = new Image(); /* I like this better */
image->read("unicorn.jpeg");

Бывает трудно рассуждать о том, как работает программа на C ++, если в конструкторах содержится много кода, особенно если вы задаете вопрос: "что произойдет, если конструктор выйдет из строя?" Это главное преимущество переноса кода из конструкторов.

Мне хотелось бы сказать больше, но я не знаю, что вы пытаетесь сделать с задержкой строительства.

Редактировать:Я вспомнил, что существует (несколько извращенный) способ вызвать конструктор для объекта в любое произвольное время.Вот один из примеров:

class Counter {
public:
    Counter(int &cref) : c(cref) { }
    void incr(int x) { c += x; }
private:
    int &c;
};

void dontTryThisAtHome() {
    int i = 0, j = 0;
    Counter c(i);       // Call constructor first time on c
    c.incr(5);          // now i = 5
    new(&c) Counter(j); // Call the constructor AGAIN on c
    c.incr(3);          // now j = 3
}

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

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

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

public:
  MyClass() : field_(0) { } // constructor, additional initializers and code omitted
  ~MyClass() {
    if (field_)
      delete field_; // free the constructed object only if initialized
  }
  ...
private:
  ...
  field_type* field_;

далее, вместо использования указателя я бы получил доступ к полю с помощью следующего метода:

private:
  ...
  field_type& field() {
    if (!field_)
      field_ = new field_type(...);
    return field_;
  }

Я опустил семантику постоянного доступа

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

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

Пример:

Object *x = (Object *) malloc(sizeof(Object));
//Use the object member items here. Be careful: no constructors have been called!
//This means you can assign values to ints, structs, etc... but nested objects can wreak havoc!

//Now we want to call the constructor of the object
new(x) Object(params);

//However, you must remember to also manually call the destructor!
x.~Object();
free(x);

//Note: if you're the malloc and new calls in your development stack 
//store in the same heap, you can just call delete(x) instead of the 
//destructor followed by free, but the above is the  correct way of 
//doing it

Лично я использовал этот синтаксис единственный раз, когда мне пришлось использовать пользовательский распределитель на основе C для объектов C ++.Как предлагает Дитрих, вам следует задаться вопросом, действительно ли вы должны отложить вызов конструктора.В База конструктор должен выполнять минимум, чтобы привести ваш объект в работоспособное состояние, в то время как другие перегруженные конструкторы могут выполнять больше работы по мере необходимости.

Я не знаю, существует ли для этого формальный шаблон.В тех местах, где я это видел, мы называли это "ленивый", "по требованию" или "по требованию".

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top