Forcing uninitialised declaration of member with a default constructor

Viewed 2171

I discovered this phenomenon today, where a member is unnecessarily constructed twice:

#include <iostream>

class Member {
public:
    Member() {
        std::cout << "Created member (default)" << std::endl;
    }

    Member(int i) {
        std::cout << "Created member: " << i << std::endl;
    }
};

class Object {
    Member member;

public:
    Object() {
        member = 1;
    }
};

int main() {
    Object o;
    return 0;
}

Is there a way to declare the member uninitialised - instead of using the default constructor - hence forcing you to use the initialiser lists in the constructor?

In Java, if you define a member like so: Member i; and you don't initialise it in every constructor, you'll get an error saying the field may be uninitialised, when trying to use it.

If I remove the default constructor from the Member class, I get the behaviour I want - the compiler forces you to use an initialiser list for every constructor - but I want this to happen in general, to stop me from forgetting to use this form instead (when a default constructor is available).


Essentially, I wanted protection against mistakenly using the default constructor, but it looks like this doesn't exist...

Even when marking the constructor with the explicit keyword, Member member still generates a member - that's immediately discarded when it's reassigned in the constructor. This itself seems inconsistent as well...

My main problem is the inconsistency. You can declare an uninitialised member if it has no default constructor; this is actually useful; you don't need to feed an initial redundant declaration, but simply initialise at the constructor (and break if not initialised). This functionality is completely missing for classes with a default constructor.


A related example is:

std::string s;
s = "foo"; 

You could simply do: std::string s = "foo"; instead, however if "foo" is actually multiple lines - as opposed to a single expression - we get non-atomic initialisation.

std::string s = "";
for (int i = 0; i < 10; i++) s += i;

This initialisation could easily end up in a torn write.

If you split it up, like so, it's assigned nearly atomically, however you still have the default value used as a placeholder:

std::string member;
// ...
std::string s = "";
for (int i = 0; i < 10; i++) s += i;
member = s; 

In this code, you could actually simply move the member variable down after s is fully constructed; however, in a class, this isn't possible, as a member with a default constructor must be initialised at decleration - despite members without a default constructor not being restricted in the same way.

In the above case, the redundant use of std::string's default constructor is relatively inexpensive, but that wouldn't hold for all situations.


I don't want the default constructor gone, I just want an option to leave the member uninitialised until the constructor - the same way I can with types with no default constructor. To me, it seems like such a simple feature and I'm puzzled by why it's not supported/

It seems that this would have naturally been implemented (whenever uninitialised declaration of types with no default constructor was) if not for bracketless instantiation of a class being supported, which presumptuously instantiates classes - even when you want them left uninitialised, like my situation.


EDIT: Running into this problem again

In java you can do this

int x; // UNINITIALISED
if (condition){
   x = 1; // init x;
}
else return;
use(x); // INITIALISED

In c++ this is not possible??? It initialises with the default constructor, but this isn't necessary - its wasteful. - note: you can not use the uninitialised variable. As you can see, because I'm using x outside of the loop, it has to get declared there, at which point it's - unnecessarily - initialised. Another scenario where int x = delete would be useful. It would break no code, and only cause a compile-time error when trying to use the uninitialised x. There's no uninitialised memory or undeterministic state, it's simply a compile-time thing - that Java has been able to implement well.

6 Answers

It's important to remember that C++ is not Java. In C++, variables are objects, not references to objects. When you create an object in C++, you have created an object. Calling a default constructor to create an object is just as valid as calling any other constructor. In C++, once you enter the body of a class's constructor, all of its member subobjects are fully-formed objects (at least, as far as the language is concerned).

If there is some type which has a default constructor, that means that it is 100% OK for you to use that default constructor to create an instance of that type. Such an object is not "uninitialized"; it is initialized via its default constructor.

In short, it is wrong for you to consider a default constructed object "uninitialized" or otherwise invalid. Not unless that default constructor explicitly leaves the object in a non-functional state.

I don't want the default constructor gone, I just want an option to leave the member uninitialised until the constructor - the same way I can with types with no default constructor.

Again, C++ is not Java. The term "uninitialized" in C++ means something completely different than when you're dealing with Java.

Java declares references, C++ declares objects (and references, but they have to be bound immediately). If an object is "uninitialized", it is still an object in C++. The object has undefined values, and thus you are limited in how you may access it. But it is still a complete and total object as far as C++'s object model is concerned. You can't construct it later (not without placement-new).

In Java, to leave a variable uninitialized means that there is no object; it's a null reference. C++ has no equivalent language concept, not unless the member in question is a pointer to an object rather than the object itself. Which is a pretty heavy-weight operation.

In any case, in C++, the author of a class has the right to restrict how that class works. This includes how it gets initialized. If the author of a class wants to ensure that certain values in that object are always initialized, then they get to do that and there is nothing you can do to stop it.

Generally speaking, you should avoid trying to do what you're doing. If however there is some type that you must initialize outside of the constructor member initializer list, and you don't want to call its default constructor (or it doesn't have one), then you can use std::optional<T>, where T is the type in question. optional is what it sounds like: an object that may or may not hold a T. Its default constructor starts without a T, but you can create a new T with optional::emplace. And you can access the T with pointer syntax like -> or *. But it never heap-allocates the T, so you don't have that overhead.

One solution would be to provide a simple generic wrapper that prevents default construction, while allowing for all other use cases. It needn't be much; a naïve approach like this, for example, should do the task well enough.1

#include <utility> // std::forward()

template<typename T>
class NoDefaultConstruct {
    T data;

// All member functions are declared constexpr to preserve T's constexpr-ness, if applicable.
public:
    // Prevents NoDefaultConstruct<T> from being default-constructed.
    // Doesn't actually prevent T itself from being default-constructed, but renders T's
    //  default constructor inaccessible.
    constexpr NoDefaultConstruct() = delete;

    // Provides pass-through access to ALL of T's constructors, using perfect forwarding.
    // The deleted constructor above hides pass-through access to T's default constructor.
    template<typename... Ts>
    constexpr NoDefaultConstruct(Ts&&... ts) : data{std::forward<Ts>(ts)...} {}

    // Allow NoDefaultConstruct<T> to be implicitly converted to a reference to T, allowing
    //  it to be used as a T& in most constructs that want a T&.  Preserves const-ness.
    constexpr operator T&()       { return data; }
    constexpr operator T&() const { return data; }
};

If we then use this in Object...

class Object {
    //Member member;
    NoDefaultConstruct<Member> member;

public:
    // Error: Calls deleted function.
    //Object() {
    //    member = 1;
    //}

    Object() : member(1) {}
};

...We are now required to explicitly initialise member in the initialiser list, due to the original Object default constructor's implicit call to decltype(member)() being sent on a shady detour through NoDefaultConstructville's deleted back alleys.


1: Note that while NoDefaultConstruct<T> will behave more-or-less identically to T in most cases, there are exceptions. The most noticeable is during template argument deduction, along with anywhere else that template argument deduction rules are used.

There is no such feature in any mainstream C++ compiler. How do I know? Because it would break (or warn about) basically every existing C++ library. What you're asking for doesn't exist, but moreover cannot exist in a compiler which compiles C++.

So based on the discussion we've had in the comments, it sounds like something this might fit the bill of what you're looking for?
As has been mentioned the exact thing you're looking for in C++ doesn't exist, but there are language features to get you pretty close i think.

template <typename T>
struct must_init
{
    using type = std::remove_cvref_t<T>;
    type t;
    must_init(type&& t) : t{std::move(t)} {}
};

If you wrap a class in this, you won't be able to compile without assigning to it. i.e.

class A
{
    must_init<std::string> s;
};

will give you a compiler error saying s has to be initialized, whereas if you define it like

class A
{
    A() : s{""} {}
    must_init<std::string> s;
};

this will compile. You can call the default constructor like so

class A
{
    A() : s{{}} {}
    must_init<std::string> s;
};

godbolt. https://godbolt.org/z/e_nsRQ

I had this issue too as I used to develop in Java as my first language and I'm switching to C++ for personal reasons (I needed lower access level).

In my particular case, I have a sf::Thread object that I wanted to declare inside a class, without initializing it. As this is part of SFML, I cannot change its implementation.

In a different section of the project, I want to actually create the thread and start it.

I was hitting compilation issues due to sf::Thread not being instantiated, because when you declare it as a member of a class the constructor is automatically called.

After some research, I found about Smart Pointers, for example std::unique_ptr. This pointer owns and manages another object. I found it useful for what I wanted to accomplish, with the drawback that you have to deal with a pointer (So you should free it once you're done with it):

// Somewhere (In your case in your main function, in my case it was a member of another class)
std::unique_ptr<sf::Thread> thread_background;

// In the calling method
void start_thread_background(){
    thread_background.reset(new sf::Thread(/*sf::Thread arguments*/));
    thread_background->launch(); // You must call methods with -> because it's a pointer now
}

You have run up against two intentional design decisions of C++ which prevent you from creating the workflow check you want (you're warned against initializing a member in a constructor body when you could have used its initializer list) using language features alone.

One, the designers of C++ decided to make it so there is no such thing as an uninitialized object. This is why constructors can do only two things - make a functional object, or throw an exception. Uninitialized types can cause headaches (and in C++ uninitialized integers often do) so the designers eliminated that state from objects entirely when they wrote the language spec.

Two, the designers also decided that all objects should have an automatically generated default constructor, unless certain conditions are met. (For example, the presence of a user-written default constructor, user deletion of the default constructor with the =delete syntax, or a member which itself cannot be default initialized, such as a reference type.)

You can't get what you want using language features alone, so you can either get what you want using extensions such as a linter, or you can change what you want.

I'd recommend the latter, i.e. adapting to the typical C++ way of doing things. It will cause you less friction when using the language. Specifically I'd recommend embracing the deletion of default constructors when you really want to express "this object cannot be constructed without additional information," and in all other cases, just acquiring the habit of initializing class members in constructor initializer lists. You want to follow best practice by doing this, but unfortunately there's no straightforward way to build yourself a guardrail, you just have to pay attention and enforce the rule on yourself.

There may be linters which you can get to generate warnings when a member is initialized in the constructor body when it could have been initialized in the initializer list, but I'm not personally aware of any.

Related