Question

Move semantics are great for RAII classes. They allow one to program as if one had value semantics without the cost of heavy copies. A great example of this is returning std::vector from a function. Programming with value semantics however means, that one would expect types to behave like primitive data types. Those two aspects sometimes seem to be at odds.

On the one hand, in RAII one would expect the default constructor to return a fully initialized object or throw an exception if the resource acquisition failed. This guarantees that any constructed object will be in a valid and consistent state (i.e. safe to use).

On the other hand, with move semantics there exists a point when objects are in a valid but unspecified state. Similarly, primitive data types can be in an uninitialized state. Therefore, with value semantics, I would expect the default constructor to create an object in this valid but unspecified state, so that the following code would have the expected behavior:

// Primitive Data Type, Value Semantics
int i;
i = 5;

// RAII Class, Move Semantics
Resource r;
r = Resource{/*...*/}

In both cases, I would expect the "heavy" initialization to occur only once. I am wondering, what is the best practice regarding this? Obviously, there is a slight practical issue with the second approach: If the default constructor creates objects in the unspecified state, how would one write a constructor that does acquire a resource, but takes no additional parameters? (Tag dispatching comes to mind...)

Edit: Some of the answers have questioned the rationale of trying to make your classes work like primitive data types. Some of my motivation comes from Alexander Stepanov's Efficient Programming with Components, where he talks about regular types. In particular, let me quote:

Whatever is a natural idiomatic expression in c [for built-in types], should be a natural idiomatic expression for regular types.

He goes on to provide almost the same example as above. Is his point not valid in this context? Am I understanding it wrong?

Edit: As there hasn't been much discussion, I am about to accept the highest voted answer. Initializing objects in a "moved-from like" state in the default constructor is probably not a good idea, since everyone who agreed with the existing answers would not expect that behavior.

Was it helpful?

Solution

Programming with value semantics however means, that one would expect types to behave like primitive data types.

Keyword "like". Not "identically to".

Therefore, with value semantics, I would expect the default constructor to create an object in this valid but unspecified state

I really don't see why you should expect that. It doesn't seem like a very desirable feature to me.

what is the best practice regarding this?

Forget this idea that a non POD class should share this feature in common with primitive data types. It's wrong headed. If there is no sensible way to initialize a class without parameters, then that class should not have a default constructor.

If you want to declare an object, but hold off on initializing it (perhaps in a deeper scope), then use std::unique_ptr.

OTHER TIPS

If you accept that objects should generally be valid by construction, and all possible operations on an object should move it only between valid states, then it seems to me that by having a default constructor, you are only saying one of two things:

  • This value is a container, or another object with a reasonable “empty” state, which I intend to mutate—e.g., std::vector.

  • This value does not have any member variables, and is used primarily for its type—e.g., std::less.

It doesn’t follow that a moved-from object need necessarily have the same state as a default-constructed one. For example, an std::string containing the empty string "" might have a different state than a moved-from string instance. When you default-construct an object, you expect to work with it; when you move from an object, the vast majority of the time you simply destroy it.

How would one write a constructor that does acquire a resource, but takes no additional parameters?

If your default constructor is expensive and takes no parameters, I would question why. Should it really be doing something so expensive? Where are its default parameters coming from—some global configuration? Maybe passing them explicitly would be easier to maintain. Take the example of std::ifstream: with a parameter, its constructor opens a file; without, you use the open() member function.

What you can do is lazy initialization: have a flag (or a nulled pointer) in your object that indicates whether the object is fully initialized. Then have a member function that uses this flag to ensure initialization after it is run. All your default constructor needs to do is to set the initialization flag to false. If all members that need an initialized state call ensure_initialization() before starting their work, you have perfect semantics and no double heavy initialization.

Example:

class Foo {
public:
    Foo() : isInitialized(false) { };

    void ensureInitialization() {
        if(isInitialized) return;
        //the usual default constructor code
        isInitialized = true;
    };

    void bar() {
        ensureInitialization();
        //the rest of the bar() implementation
    };

private:
    bool isInitialized;
    //some heavy variables
}

Edit: To reduce the overhead produced by the function call, you can do something like this:

//In the .h file:
class Foo {
public:
    Foo() : isInitialized(false) { };
    void bar();

private:
    void initialize();

    bool isInitialized;
    //some heavy variables
}

//In the .cpp file:
#define ENSURE_INITIALIZATION() do { \
    if(!isInitialized) initialize(); \
} while(0)

void Foo::bar() {
    ENSURE_INITIALIZATION();
    //the rest of the bar() implementation
}

void Foo::initialize() {
    //the usual default constructor code
    isInitialized = true;
}

This makes sure that the decision to initialize or not is inlined without inlining the initialization itself. The later would just bloat the executable and reduce instruction cache efficiency, but the first can't be done automatically, so you need to employ the preprocessor for that. The overhead of this approach should be less than a function call on average.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top