Basically, this all boils down to which design to choose from the following three:
Designs
Disclaimer: this post is not encouraging the use of exception specifications or exceptions for that matter. The errors may equivalently be reported using error codes if you wish. Exception specifications as used here are just meant to illustrate when different errors can occur using a concise syntax.
Design 1
This is the most recurring design out there, and totally non-RAII. The constructor just puts the object in some stale state and each instance must be initialized manually after construction takes place.
class SecureStream
{
public:
SecureStream();
void initialize(Stream&,const Key&) throw(InvalidKey,AlreadyInitialized);
std::size_t get( void*,std::size_t) throw(NotInitialized,IOError);
std::size_t put(const void*,std::size_t) throw(NotInitialized,IOError);
};
Pros:
- Users have control over when to invoke the "heavy" initialization process
- The object can be created before the key exists. This is important for frameworks such as COM, where all objects must have a default constructor (the
CoCreateObject()
does not allow you to forward extra arguments the object constructor). Sometimes, there are still workarounds, such as a builder object.
Cons:
- Objects must be checked for the stale state before using the object. This may be enforced by the object by returning an error code or throwing an exception. Personally, I hate objects that allow me to use them and just appear to ignore my calls (e.g. a failed
std::ostream
).
Design 2
This is the RAII approch. Make sure the object is 100% usable with no extra artefacts (e.g. manually calling stream.initialize(...);
on each instance.
class SecureStream
{
public:
SecureStream(Stream&,const Key&) throw(InvalidKey);
std::size_t get( void*,std::size_t) throw(IOError);
std::size_t put(const void*,std::size_t) throw(IOError);
};
Pros:
- The object can always be assumed to be in a valid state. This is so much simpler to use.
Cons:
- Constructor might take a long time to execute.
- All required arguments must be available at the instance construction. This has once in a while been a problem for me, especially if most other objects in the code base use design #1.
Design 3
Somewhat of a compromise between the two previous cases. Don't initialize yet, but have the other methods lazily invoke the internal .initialize(...)
method when necessary.
class SecureStream
{
public:
SecureStream(Stream&,const Key&);
std::size_t get( void*,std::size_t) throw(InvalidKey,IOError);
std::size_t put(const void*,std::size_t) throw(InvalidKey,IOError);
private:
void initialize() throw(InvalidKey);
};
Pros:
- Almost as easy to use as design #1. Almost (see below).
Cons:
- If the initialization step may fail, it may now fail anywhere there is a first call to any of the public methods. Proper error handling for this scenario is extremely difficult.
Discussion
If you absolutely must pay for the initialization for every instance, then design #1 is out of the question as it just results in more bugs in the software.
The question is just about when to pay for the initialization cost. Do you prefer paying it upfront, or on first use? In most scenarios, I prefer paying upfront because I don't want to assume users can handle errors later in the program. However, there might be specific threading semantics in your program, and you might not be able to stall threads at creation time (or, conversely, at use time).
In any case, you can still get the benefits of design #3 by using dynamic allocation of the class in design #2.
Conclusion
Basically, if the only reason you are hesitating is for some philosophical ideal where constructors execute quickly, I would just go with the pure RAII design.