What should the default constructor do in a RAII class with move semantics?
Asked Answered
B

3

11

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.

Berck answered 11/8, 2013 at 4:19 Comment(2)
There are different kinds of RAII classes: there are those that require parameters to describe the resource they acquire, and those that do not. Which is yours?Rivulet
The RAII classes that I write are mostly wrappers around C libraries like OpenGL, SDL or FFmpeg. Sometimes acquiring resources requires parameters and sometimes it does not. I wanted to universally define the behavior of the default constructor for all wrappers (to minimize potential confusion).Berck
S
7

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.

Surcharge answered 11/8, 2013 at 5:15 Comment(8)
+1 for mentioning unique_ptr; I have found it useful for lazy initialisation.Palatal
I think your answer is very valid and this is one viable way to approach the issue. However, I think some people might disagree. For example, see Alexander Stepanov talking about regular types (youtube.com/watch?v=DOoO7_yvjQE&t=14m00s).Berck
A quick summary of my understanding of what Alexander Stepanov is saying in the previously linked video: He designed STL to work with types that are like built-in types (and he goes on to talk about the basic operations). So, I think there may be value in making these basic operations of your type behave like a primitive data type.Berck
@kloffy: I've watched up to the 30 minute mark so far, but I see no evidence that he would disagree with me. He is just defining "regular type", not saying that every type should be a regular type.Surcharge
Yes, my point is that since he designed STL for use with with regular types, it may be beneficial to make your custom types behave like regular types. Further, his point that a natural idiomatic expression for built-in types should also work with custom types resonates with me.Berck
@Berck You seem to be conflating two very different points. Stepanov designed the STL to not exclude basic types: in other words, to work with user-defined types as well as basic types. That is not the same as saying that all custom types should implement all the syntax that basic types have. Stepanov's point was that the STL should not, for example, define vector to only be able to store types subclassed from Object or from IContainerStorable. It was about enabling flexibility, so that the STL will be usable no matter which type you try to use it withCervical
He wanted to avoid placing requirements on types. That is the exact opposite of what you seem to interpret it as, which is basically adding extra requirements to types (they must be able to do everything that basic types can do). You should design your types to express what makes sense for the concepts they represent (and so they shouldn't have a default ctor unless the default ctor is meaningful), and the STL (and any other well-designed library) should be able to work with those constraints, and not place unnecessary requirements on your typesCervical
When did I suggest that "all custom types should implement all the syntax that basic types have"? I am well aware that the STL will work just fine for any custom type. But really, my question has nothing to do with STL or any other library in particular. I brought up the video because of the bit where he talks about idiomatic expressions. My aim is to define a consistent behavior for the default constructors of my RAII wrappers (trying to follow the principle of least astonishment).Berck
P
6

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.

Palatal answered 11/8, 2013 at 5:28 Comment(11)
Yes, you are absolutely right that there is no rule anywhere that a default-constructed object should be in a similar state to a moved-from object. However, I argue that based on the behavior of built-in types this might be a natural choice (at least in the case of RAII).Berck
However, if I was sure about this, I wouldn't have posted the question... ;)Berck
@Berck It is actually a very bad choice to put a moved-from object in a default-initialized state, as this is usually far less efficient than simply swapping the state of the moved-to and moved-from object. And as the state must be valid but is not specified, there is no reason to do something less efficient.Kerwinn
@Kerwinn Actually, what I was suggesting was the other way round. I was considering putting default-constructed objects in a moved-from state. This way, default-construction would be very lightweight and the object would just be a container to move into (I was drawing a parallel to uninitialized primitive types).Berck
@Joe: You can't generally "simply swap" in order to implement move. The resources owned by the moved-to object need to get freed, not owned by the moved-from object. The state of the moved-from object is left unspecified, but the state of the resources formerly contained in the moved-to object should be clearly specified.Extensor
@BenVoigt That isn't really necessary, as the moved-from object will go out of scope (or assigned with a new value if you actually do continue to use it) anyways and the resources will be freed then.Kerwinn
@Joe: It might go out of scope immediately, and it might not for a long time. Making such an assumption in the move constructor / move assignment operator is a terrible terrible idea.Extensor
@BenVoigt Not much of an argument IMO, as keeping something around "for a long time" which is not used (as the state is valid but unspecified this means assigning to which would lead to freeing the old resource) is also a "terrible terrible idea"Kerwinn
@Joe: Not really. The state of the object moved from is unspecified, but the state of object moved to is completely specified -- the resources it used to own are freed, (not transferred to another object) and it now owns the resources formerly owned by the moved-from object. The moved-from object no longer owns any resources, and so it doesn't really matter how long it lives.Extensor
@BenVoigt Both variants leave the moved-to object in a specified state (the previous state of the moved-from object). Which semantic is better really depends on the resource in question. For example a vector that keeps the memory (but destroys the contents) when moving would not violate the spec (Table 96 container requirements only requires that the elements are moved to or destroyed), and would be better for performance if the moved-from vector is then filled again (after a clear to get a specified state), and if not the memory will be freed at the end of the scope.Kerwinn
@Joe: Right, by "resources" I'm not referring to memory or other internal state, only externally visible state such as locks, mutexes, open file handles.Extensor
P
2

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.

Primitivism answered 17/8, 2013 at 7:15 Comment(2)
Thanks for suggesting another interesting alternative. Certainly one way to guard against errors by users of the class. Slightly burdensome to put ensureInitialization into all methods, though.Berck
That's true, and it's the reason you have to think about the question: What overhead is greater? Unnecessary construction or one function call with one condition evaluated for every method call? Avoiding an unnecessary call to new/delete, however, means avoiding more than 200 CPU cycles, so you have to call ensureInitialization() quite a number of times to make up for one saved initialization. But thinking about the overhead, I'll add a little optimization to my answer :-)Primitivism

© 2022 - 2024 — McMap. All rights reserved.