How much work should constructor of my class perform?
Asked Answered
J

4

6

I have a class that represents a data stream, it basically reads or writes into a file, but first the data are being encrypted/decrypted and there is also an underlying codec object that handles the media being accessed.

I'm trying to write this class in a RAII way and I'd like a clean, nice, usable design.

What bothers me is that right now there is a lot of work being done in the constructor. Before the object's I/O routines can be safely used, first of all the codec needs to initialized (this isn't very demanding), but then a key is taken into account and crypto and other things are intialized - these require some analysis of the media which takes quite a lot of computation.

Right now I'm doing all this in the constructor, which makes it take a long time. I'm thinking of moving the crypto init stuff (most work) out of the ctor into a separate method (say, Stream::auth(key)), but then again, this would move some responsibility to the user of the class, as they'd be required to run auth() before they call any I/O ops. This also means I'd have to place a check in the I/O calls to verify that auth() had been called.

What do you think is a good design?

P.S. I did read similar question but I wasn't really able to apply the answers on this case. They're mostly like "It depens"... :-/

Thanks

Jocko answered 18/1, 2012 at 19:10 Comment(3)
I'm not sure why you consider a long constructor a bad thing in the first place...? If there's a lot of initialization, the constructor is going to be long, period.Oversubtle
You mention that you don't like this initialization in the constructor because it takes time. How does putting it in a separate function reduce this time? Are these objects often constructed and destroyed without being used?Doerr
The only correct answer is "it depends". :)Hardware
H
8

The only truly golden unbreakable rule is that the class must be in a valid, consistent, state after the constructor has executed.

You can choose to design the class so that it is in some kind of "empty"/"inactive" state after the constructor has run, or you can put it directly into the "active" state that it is intended to be in.

Generally, it should be preferred to have the constructor construct your class. Usually, you wouldn't consider a class fully "constructed", until it's actually ready to be used, but exceptions do exist. However, keep in mind that in RAII, one of the key ideas is that the class shouldn't exist unless it is ready, initalized and usable. That's why its destructor does the cleanup, and that's why its constructor should do the setup.

Again, exceptions do exist (for example, some RAII objects allow you to release the resource and perform cleanup early, and then have the destructor do nothing.) So at the end of the day, it depends, and you'll have to use your own judgment.

Think of it in terms of invariants. What can I rely on if I'm given an instance of your class? The more I can safely assume about it, the easier it is to use. If it might be ready to use, and might be in some "constructed, but not initialized" state, and might be in a "cleaned up but not destroyed" state, then using it quickly becomes painful.

On the other hand, if it guarantees that "if the object exists, it can be used as-is", then I'll know that I can use it without worrying about what was done to it before.

It sounds like your problem is that you're doing too much in the constructor.

What if you split the work up into multiple smaller classes? Have the codec be initialized separately, then I can simply pass the already-initialized codec to your constructor. And all the authentication and cryptography stuff and whatnot could possibly be moved out into separate objects as well, and then simply passed to "this" constructor once they're ready.

Then the remaining constructor doesn't have to do everything from scratch, but can start from a handful of helper objects which are already initialized and ready to be used, so it just has to connect the dots.

Hardware answered 18/1, 2012 at 19:28 Comment(2)
Ok, I've decided I'll most likely have the codec initialized and pre-analyzed the media in advance and then have it passed to stream's ctor. I've found out that this will actually not change my API much, the API is basically ready for this ;-) (P.S. I'll keep the old constructor too though, it maybe be more suitable in some cases) Thanks for your answer!Jocko
+1 Very very informative and to the point. But there are lots of buts, but other than that, true gold. But remove some buts. :)Gwenore
P
2

you could just place the check in the IO calls to see if auth has been called, and if it has, then continue, if not, then call it.

this removes the burden from the user, and delays the expense until needed.

Persinger answered 18/1, 2012 at 19:13 Comment(4)
I'm not c++ programmer, but I generally try to put the bare minimum into the constructor in any OO application. I like to do everything else lazily as possible, so right before the method that requires it is called. Lots my methods in a large system might have code that checks if a property is defined, if so returns it, if not calculates it. I use getters of some kind to facilitate this. If the languages doesn't support getters, I make methods whose names are prefixed with getThermoelectric
I'm afraid I can't do that, beacuse the auth step needs the key. I might let the key be passed to the constructor though...Jocko
Doing everything lazily makes it nearly impossible to predict the behavior. Handling auth errors is much much harder if they occur at some unpredictable time during some unknown other call when I try to use the class. If I can do it explicitly through an auth() function, or through the constructor, then I know when and where to handle auth errors, and I have one less error to worry about when using the class otherwise.Hardware
@jalf: I totally share this concern. I've submitted an answer that discusses this point more explicitly.Perales
B
2

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:

  1. Users have control over when to invoke the "heavy" initialization process
  2. 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:

  1. 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:

  1. The object can always be assumed to be in a valid state. This is so much simpler to use.

Cons:

  1. Constructor might take a long time to execute.
  2. 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:

  1. Almost as easy to use as design #1. Almost (see below).

Cons:

  1. 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.

Blanka answered 18/1, 2012 at 19:52 Comment(0)
S
-1

There's no hard and fast rule on this, but in general it's best to avoid heavy constructors for two reasons that come to mind (maybe others as well):

  • The order of the objects created intializer list can give rise to subtle bugs
  • What to do with exceptions in the constructor? Will you need to handle partially-constructed objects in your app?
Seabrooke answered 18/1, 2012 at 19:19 Comment(3)
How is object initialization order related to the amount of work (in execution time) performed by the constructor? Also, "partially constructed" objects are never a result of exceptions, only a result of not fully initializing the object in the constructor.Perales
@Andre It's not, I'm just commenting off of the OP's second paragraph regarding clean design.Seabrooke
If you're just commenting, then consider make this comment a comment, not an answer.Perales

© 2022 - 2024 — McMap. All rights reserved.