Ленивая / многоступенчатая конструкция в C ++ - PullRequest
7 голосов
/ 28 января 2010

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

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

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

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

Ответы [ 6 ]

4 голосов
/ 28 января 2010

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

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

Если вы отличаете полностью заполненные объекты от не полностью заполненных объектов на уровне типа, вы можете применить требование, чтобы функция передавалась как завершенный объект. Для этого я бы предложил создать соответствующий тип 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&, поэтому этот подход подчиняется подстановке Liskov Принцип . Это означает, что функция может использовать тип параметра X&, чтобы указать, что ей нужен полный объект X, или XPartial&, чтобы указать, что она может обрабатывать частично заполненные объекты - в этом случае либо объект XPartial, либо полный X может быть передан.

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

1 голос
/ 28 января 2010

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

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 (и, возможно, встроенным), а указатель Big должен быть изменяемым. И, конечно же, A должен иметь возможность создавать Big, что в других случаях может означать кэширование параметров конструктора содержащегося в нем класса. Вам также нужно будет принять решение о политике копирования / назначения - я бы, вероятно, запретил и то, и другое для этого типа класса.

0 голосов
/ 28 января 2010

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

0 голосов
/ 28 января 2010

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

В основном: зарезервируйте объект, используя 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 ++. Как предполагает Дитрих, вам следует задаться вопросом, действительно ли вы действительно должны откладывать вызов конструктора. Конструктор base должен выполнить минимум, чтобы перевести ваш объект в исправное состояние, в то время как другие перегруженные конструкторы могут выполнять больше работы по мере необходимости.

0 голосов
/ 28 января 2010

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

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

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_;
  }

Я опустил семантику const-access

0 голосов
/ 28 января 2010

Я не знаю ни одного паттерна для решения этой конкретной проблемы. Это сложный вопрос дизайна, и он несколько уникален для таких языков, как 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
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...