How to lazy init class member without a default constructor

195 views Asked by At

How do I lazy init class members without a default constructor?

Here is the minimum reproducible example:

#include <stdio.h>
#include <string>
#include <iostream>
using namespace std;
class B{
public:
    string name;
    B(string n):name(n){
        cout <<"B construct by name" <<endl;
    };
    B(){
        cout <<"B construct by deafult" <<endl;
    }
};
class A{
public:
    B b;
    A(){
        this->b = B{"name"}; 
        /* 
          I want to set this->b here, 
          not set by init list, 
          without B deafult construct(happen when A delcare).
        */
    };
};
int main()
{
    A a;
    return 0;
}

If you run the minimum reproducible example, it would print:

B construct by default
B construct by name

Can avoid the first log? Should I use std::optional, or is my design, as it is, is fundamentally wrong?

3

There are 3 answers

7
user12002570 On BEST ANSWER

I want to set this->b here, not set by init list...

One other option with modern C++ is to use in-class initializer as shown below.

class B{
public:
    string name;
    B(string n):name(n){
        cout <<"B construct by name" <<endl;
    };
    B(){
        cout <<"B construct by deafult" <<endl;
    }
};
class A{
public:
    B b{"name"}; //use in-class initializer
    A(){
    
    };
};

Now the output is as you want:

B construct by name

Working demo

2
Chris Kushnir On

By the time the constructor body is called all members have been initialized, either default or in init list. The exception are things that don't have default initialization e.g. anonymous unions. So in A you could replace

B b;

with

union {
    B b;
};

But this has it's own issues i.e. you now need an explicit A::~A to destruct b, and this->b = B{"name"}; will likely crash, as the assign will assume the current b is constructed (which it isn't) which will likely cause it try and release B::name which is currently full of garbage. So you'd have init b in some form before the assign, or do a custom assign that 'somehow' knows if the current instance is init or not ... would likely lead to a mess.

tldr; technically possible, but will likely shoot yourself in the foot. just let it default init b and make sure that the default init of B is cheap.

1
AndrewBloom On

Unfortunately it seems it's not possible to mimic exactly the behaviour of other languages, like Kotlin, that allow to late initialise variables. Problems that are connected to this are the implementations of proxy classes and smart reference among the others, which currently are not possible too. Following is my attempt to mimic the requested behaviour as close as possible.

For late initialisation, we need to decouple the phases of allocation of memory and construction of the object. A possible solution would be to wrap the class with a template and leverage the placement new operator:

template<class T> class LateInit {
private:
    char buf[sizeof(T)];
    bool initd;
public:
    LateInit(): initd(false) {}
    
    template<typename ...Args> T& operator()(Args ...args) {
        initd = true;
        return *new (buf) T(args...);
    }
    // the painful point, operator cast doesn't cast implicitly with dot operator,
    // which acts on LateInit class and not T. :'( Sadness.
    operator T&() { return *reinterpret_cast<T*>(buf); }
    ~LateInit() { if(initd) reinterpret_cast<T*>(buf)->~T(); }
     // for debbugging purpose only, not needed
    void *getAddress() { return buf; }
};

a buf is created with the size of the object to late init. The call operator is used to forward the arguments to the constructor of T, which leverages the placement new to use the memory allocated by the buffer buf. An operator cast casts the the class to a reference to T. Moreover, a boolean flag it's used to call the destructor for the T object only if needed when the LateInit object goes out of scope (getAddress it's only to print the address for debugging).

The original idea was to use it like this:

// This unfortunately has very limited usability
LateInit<B> b; // only allocates, no constructor called
...
b("name"); // constructs b in the pre-allocated memory
...
b.aMethod(); // Ouch! doesn't work! b it's not implicitly converted here!
((B)b).aMethod(); // this works, but wrapping all calls with an explicit cast sucks badly

It can be seen that due to the lack of the implicit conversion (or the ability to overload the dot operator, or any other equivalent mechanism), it's impossible to use LateInit as a B object transparently. So unfortunately, the closest would be the following:

class A{
public:
    LateInit<B> LI_b;
    B& b;
    A(): b(LI_b) {
        this->LI_b("name");
    };
};

At the cost of using an extra-reference, we can link the reference to the memory at A's construction time, and we need to call the constructor using LI_b. after that, we can use the object b through the reference, as in:

int main()
{
    A a;
    a.b.aMethod(); // added to class B: void aMethod() { cout <<"B aMethod() called" <<endl; };
    std::cout << &a.b << " " << a.LI_b.getAddress() << std::endl;
    std::cout<<"Hello World";

    return 0;
}

which gives an output like:

B construct by name
B aMethod() called
0x7ffec51ccd30 0x7ffec51ccd30
Hello World

Important point is that the reference lifetime must be the same of the LateInit related object (as for example both members of the same class).