Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forcing uninitialised declaration of member with a default constructor

Tags:

c++

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.

like image 629
Tobi Akinyemi Avatar asked Jun 13 '20 00:06

Tobi Akinyemi


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

like image 90
Nicol Bolas Avatar answered Oct 22 '22 01:10

Nicol Bolas


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

like image 38
John Zwinck Avatar answered Oct 22 '22 00:10

John Zwinck


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.

like image 27
Justin Time - Reinstate Monica Avatar answered Oct 22 '22 01:10

Justin Time - Reinstate Monica