Should I use virtual 'Initialize()' functions to initialize an object of my class?
I'm currently having a discussion with my teacher about class design and we came to the point of Initialize()
functions, which he heavily promotes. Example:
class Foo{
public:
Foo()
{ // acquire light-weight resources only / default initialize
}
virtual void Initialize()
{ // do allocation, acquire heavy-weight resources, load data from disk
}
// optionally provide a Destroy() function
// virtual void Destroy(){ /*...*/ }
};
Everything with optional parameters of course.
Now, he also puts emphasis on extendability and usage in class hierarchies (he's a game developer and his company sells a game engine), with the following arguments (taken verbatim, only translated):
Arguments against constructors:
- can't be overridden by derived classes
- can't call virtual functions
Arguments for Initialize()
functions:
- derived class can completely replace initialization code
- derived class can do the base class initializatio开发者_StackOverflow社区n at any time during its own initialization
I have always been taught to do the real initialization directly in the constructor and to not provide such Initialize()
functions. That said, I for sure don't have as much experience as he does when it comes to deploying a library / engine, so I thought I'd ask at good ol' SO.
So, what exactly are the arguments for and against such Initialize()
functions? Does it depend on the environment where it should be used? If yes, please provide reasonings for library / engine developers or, if you can, even game developer in general.
Edit: I should have mentioned, that such classes will be used as member variables in other classes only, as anything else wouldn't make sense for them. Sorry.
For Initialize
: exactly what your teacher says, but in well-designed code you'll probably never need it.
Against: non-standard, may defeat the purpose of a constructor if used spuriously. More importantly: client needs to remember to call Initialize
. So, either instances will be in an inconsistent state upon construction, or they need lots of extra bookkeeping to prevent client code from calling anything else:
void Foo::im_a_method()
{
if (!fully_initialized)
throw Unitialized("Foo::im_a_method called before Initialize");
// do actual work
}
The only way to prevent this kind of code is to start using factory functions. So, if you use Initialize
in every class, you'll need a factory for every hierarchy.
In other words: don't do this if it's not necessary; always check if the code can be redesigned in terms of standard constructs. And certainly don't add a public Destroy
member, that's the destructor's task. Destructors can (and in inheritance situations, must) be virtual
anyway.
I"m against 'double initialization' in C++ whatsoever.
Arguments against constructors:
- can't be overridden by derived classes
- can't call virtual functions
If you have to write such code, it means your design is wrong (e.g. MFC). Design your base class so all the necessary information that can be overridden is passed through the parameters of its constructor, so the derived class can override it like this:
Derived::Derived() : Base(GetSomeParameter())
{
}
This is a terrible, terrible idea. Ask yourself- what's the point of the constructor if you just have to call Initialize()
later? If the derived class wants to override the base class, then don't derive.
When the constructor finishes, it should make sense to use the object. If it doesn't, you've done it wrong.
One argument for preferring initialization in the constructor: it makes it easier to ensure that every object has a valid state. Using two-phase initialization, there's a window where the object is ill-formed.
One argument against using the constructor is that the only way of signalling a problem is through throwing an exception; there's no ability to return anything from a constructor.
Another plus for a separate initialization function is that it makes it easier to support multiple constructors with different parameter lists.
As with everything this is really a design decision that should be made with the specific requirements of the problem at hand, rather than making a blanket generalization.
A voice of dissension is in order here.
You might be working in an environment where you have no choice but to separate construction and initialization. Welcome to my world. Don't tell me to find a different environment; I have no choice. The preferred embodiment of the products I create is not in my hands.
Tell me how to initialize some aspects of object B with respect to object C, other aspects with respect to object A; some aspects of object C with respect to object B, other aspects with respect to object A. The next time around the situation may well be reversed. I won't even get into how to initialize object A. The apparently circular initialization dependencies can be resolved, but not by the constructors.
Similar concerns goes for destruction versus shutdown. The object may need to live past shutdown, it may need to be reused for Monte Carlo purposes, and it might need to be restarted from a checkpoint dumped three months ago. Putting all of the deallocation code directly in the destructor is a very bad idea because it leaks.
Forget about the Initialize()
function - that is the job of the constructor.
When an object is created, if the construction passed successfully (no exception thrown), the object should be fully initialized.
While I agree with the downsides of doing initialization exclusively in the constructor, I do think that those are actually signs of bad design.
A deriving class should not need to override base class initialization behaviour entirely. This is a design flaw which should be cured, rather than introducing Initialize()
-functions as a workaround.
Not calling Initialize
may be easy to do accidentally and won't give you a properly constructed object. It also doesn't follow the RAII principle since there are separate steps in constructing/destructing the object: What happens if Initialize
fails (how do you deal with the invalid object)?
By forcing default initialization you may end up doing more work than doing initialization in the constructor proper.
Ignoring the RAII implications, which others have adequately covered, a virtual initialization method greatly complicates your design. You can't have any private data, because for the ability to override the initialization routine to be at all useful, the derived object needs access to it. So now the class's invariants are required to be maintained not only by the class, but by every class that inherits from it. Avoiding that sort of burden is part of the point behind inheritance in the first place, and the reason constructors work the way they do with regard to subobject creation.
Others have argued at length against the use of Initialize
, I myself see one use: laziness.
For example:
File file("/tmp/xxx");
foo(file);
Now, if foo
never uses file
(after all), then it's completely unnecessary to try and read it (and would indeed be a waste of resources).
In this situation, I support Lazy Initialization, however it should not rely on the client calling the function, but rather each member function should check if it is necessary to initialize or not. In this example name()
does not require it, but encoding()
does.
Only use initialize function if you don't have the data available at point of creation.
For example, you're dynamically building a model of data, and the data that determines the object hierarchy must be consumed before the data that describes object parameters.
If you use it, then you should make the constructor private and use factory methods instead that call the initialize()
method for you. For example:
class MyClass
{
public:
static std::unique_ptr<MyClass> Create()
{
std::unique_ptr<MyClass> result(new MyClass);
result->initialize();
return result;
}
private:
MyClass();
void initialize();
};
That said, initializer methods are not very elegant, but they can be useful for the exact reasons your teacher said. I would not consider them 'wrong' per se. If your design is good then you probably will never need them. However, real-life code sometimes forces you to make compromises.
Some members simply must have values at construction (e.g. references, const
values, objects designed for RAII without default constructors)... they can't be constructed in the initialise()
function, and some can't be reassigned then.
So, in general it's not a choice of constructor vs. initialise()
, it's a question of whether you'll end up having code split between the two.
Of bases and members that could be initialised later, for the derived class to do it implies they're not private
; if you go so far as to make bases/members non-private
for the sake of delaying initialisaton you break encapsulation - one of the core principles of OOP. Breaking encapsulation prevents base class developer(s) from reasoning about the invariants the class should protect; they can't develop their code without risking breaking derived classes - which they might not have visibility into.
Other times it's possible but sometimes inefficient if you must default construct a base or member with a value you'll never use, then assign it a different value soon after. The optimiser may help - particularly if both functions are inlined and called in quick succession - but may not.
- [constructors] can't be overridden by derived classes
...so you can actually rely on them doing what the base class needs...
- [constructors] can't call virtual functions
The CRTP allows derived classes to inject functionality - that's typically a better option than a separate initialise()
routine, being faster.
Arguments for Initialize() functions:
- derived class can completely replace initialization code
I'd say that's an argument against, as above.
- derived class can do the base class initialization at any time during its own initialization
That's flexible but risky - if the base class isn't initialised the derived class could easily end up (due to oversight during the evolution of the code) calling something that relies on that base being initialised and consequently fails at run time.
More generally, there's the question of reliable invocation, usage and error handling. With initialise
, client code has to remember to call it with failures evident at runtime not compile time. Issues may be reported using return types instead of exceptions or state, which can sometimes be better.
If initialise()
needs to be called to set say a pointer to nullptr
or a value safe for the destructor to delete
, but some other data member or code throws first, all hell breaks loose.
initialise()
also forces the entire class to be non-const
in the client code, even if the client just wants to create an initial state and ensure it won't be further modified - basically you've thrown const
-correctness out the window.
Code doing things like p_x = new X(values, for, initialisation);
, f(X(values, for initialisation)
, v.push_back(X(values, for initialisation))
won't be possible - forcing verbose and clumsy alternatives.
If a destroy()
function is also used, many of the above problems are exacerbated.
精彩评论