What should the default constructor do in a RAII class with move semantics?
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.
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
.
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.