8

什么是 C++ 中对象的多阶段构造/初始化的良好现有类/设计模式?

我有一个包含一些数据成员的类,这些数据成员应该在程序流程的不同点进行初始化,因此它们的初始化必须延迟。例如,一个参数可以从文件中读取,而另一个参数可以从网络中读取。

目前我正在使用 boost::optional 来延迟构造数据成员,但令我困扰的是 optional 在语义上与延迟构造不同。

我需要提醒 boost::bind 和 lambda 部分函数应用程序的特性,使用这些库我可能可以设计多阶段构造 - 但我更喜欢使用现有的、经过测试的类。(或者也许还有另一种我不熟悉的多阶段构建模式)。

4

6 回答 6

4

关键问题是您是否应该在类型级别区分完全填充的对象和未完全填充的对象。如果您决定不区分,那么只需使用boost::optional或类似您正在做的事情:这样可以很容易地快速获得编码。OTOH,您无法让编译器强制执行特定函数需要完全填充的对象的要求;您每次都需要对字段执行运行时检查。

参数组类型

如果您确实在类型级别将完全填充的对象与未完全填充的对象区分开来,则可以强制要求向函数传递一个完整的对象。为此,我建议XParams为每个相关类型创建一个相应的类型XXParams每个参数都有boost::optional成员和设置函数,可以在初始构造后设置。然后你可以强制X只有一个(非复制)构造函数,它以一个XParams作为唯一参数并检查每个必要的参数是否已在该XParams对象中设置。(不确定这个模式是否有名字——有人想编辑这个来填写吗?)

“部分对象”类型

如果您在对象完全填充之前不需要对它任何事情(也许不是像取回字段值这样的琐碎事情),这将非常有效。如果您有时必须将未完全填充的内容视为X"full" X,则可以改为XXPartial包含所有逻辑的 type 派生,以及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可以在任何需要 an 的地方安全地提供 an XPartial&,因此这种方法遵循Liskov Substitution Principle。这意味着一个函数可以使用参数类型X&来表示它需要一个完整的X对象,或者XPartial&表示它可以处理部分填充的对象——在这种情况下,可以传递一个XPartial对象或一个完整的对象。X

最初我有isComplete()as protected,但发现这不起作用,因为X' 的复制 ctor 和赋值运算符必须在他们的XPartial&参数上调用这个函数,并且他们没有足够的访问权限。仔细想想,公开公开这个功能更有意义。

于 2010-01-28T10:59:20.650 回答
1

我一定在这里遗漏了一些东西——我一直在做这种事情。在所有情况下都有一个类很大和/或不需要的对象是很常见的。所以动态创建它们!

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,这在其他情况下可能意味着缓存包含的类的构造函数参数。您还需要决定复制/分配政策 - 我可能会禁止此类课程。

于 2010-01-28T09:54:45.877 回答
0

我不知道有什么模式可以处理这个特定问题。这是一个棘手的设计问题,对于像 C++ 这样的语言来说有些独特。另一个问题是,这个问题的答案与您的个人(或公司)编码风格密切相关。

我会为这些成员使用指针,当需要构造它们时,同时分配它们。您可以对这些使用 auto_ptr,并检查 NULL 以查看它们是否已初始化。(我认为指针是 C/C++/Java 中内置的“可选”类型,在其他语言中 NULL 不是有效指针)。

作为风格问题的一个问题是,您可能依赖于您的构造函数来做太多的工作。当我编写面向对象的代码时,我让构造函数做足够的工作来使对象处于一致的状态。例如,如果我有一个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
}

请注意,做这种鲁莽的事情可能会引起其他程序员的鄙视,除非你有充分的理由使用这种技术。这也不会延迟构造函数,只是让您稍后再次调用它。

于 2010-01-28T08:42:25.627 回答
0

对于某些用例,使用 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_;
  }

我省略了 const-access 语义

于 2010-01-28T08:57:37.683 回答
0

我知道的最简单的方法类似于 Dietrich Epp 建议的技术,但它允许您真正延迟对象的构建,直到您选择的时刻。

基本上:使用 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 的分配器时。正如 Dietrich 建议的那样,您应该质疑您是否真的必须延迟构造函数调用。基本构造函数应该执行最少的操作以使您的对象进入可服务状态,而其他重载构造函数可能会根据需要执行更多工作。

于 2010-01-28T09:14:57.187 回答
0

我不知道这是否有正式的模式。在我见过的地方,我们称其为“懒惰”、“按需”或“按需”。

于 2010-01-28T11:10:48.293 回答