What's a good existing class/design pattern for multi-stage construction/initialization of an object in C++?
I have a class with some data members which should be initialized in different points in the program's flow, so their initialization has to be delayed. For example one argument can be read from a file and another from the network.
Currently I am using boost::optional for the delayed construction of the data members, but it's bothering me that optional is semantically different than delay-constructed.
What I need reminds features of boost::bind and lambda partial function application, and using these libraries I can probably design multi-stage construction - but I prefer using existing, tested classes. (Or maybe there's another multi-stage construction pattern which I am not familiar with).
The key issue is whether or not you should distinguish completely populated objects from incompletely populated objects at the type level. If you decide not to make a distinction, then just use boost::optional
or similar as you are doing: this makes it easy to get coding quickly. OTOH you can't get the compiler to enforce the requirement that a particular function requires a completely populated object; you need to perform run-time checking of fields each time.
If you do distinguish completely populated objects from incompletely populated objects at the type level, you can enforce the requirement that a function be passed a complete object. To do this I would suggest creating a corresponding type XParams
for each relevant type X
. XParams
has boost::optional
members and setter functions for each parameter that can be set after initial construction. Then you can force X
to have only one (non-copy) constructor, that takes an XParams
as its sole argument and checks that each necessary parameter has been set inside that XParams
object. (Not sure if this pattern has a name -- anybody like to edit this to fill us in?)
This works wonderfully if you don't really have to do anything with the object before it is completely populated (perhaps other than trivial stuff like get the field values back). If you do have to sometimes treat an incompletely populated X
like a "full" X
, you can instead make X
derive from a type XPartial
, which contains all the logic, plus protected
virtual methods for performing precondition tests that test whether all necessary fields are populated. Then if X
ensures that it can only ever be constructed in a completely-populated state, it can override those protected methods with trivial checks that always return 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!
};
Although it might seem the inheritance here is "back to front", doing it this way means that an X
can safely be supplied anywhere an XPartial&
is asked for, so this approach obeys the Liskov Substitution Principle. This means that a function can use a parameter type of X&
to indicate it needs a complete X
object, or XPartial&
to indicate it can handle partially populated objects -- in which case either an XPartial
object or a full X
can be passed.
Originally I had isComplete()
as protected
, but found this didn't work since X
's copy ctor and assignment operator must call this function on their XPartial&
argument, and they don't have sufficient access. On reflection, it makes more sense to publically expose this functionality.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With