Constructor Initializer List and Heap Allocation

Started by
7 comments, last by SOL-2517 8 years, 8 months ago

Whenever I have a field that should be initialized onto the heap upon initialization in a class, I usually initialize it to nullptr in the initializer list, and then allocate it in the constructor's body. For example:


class Test
{
private:
Model *model;

public:

Test() : model(nullptr)
{
model = new Model();
}

~Test()
{
if(model) delete model;
}

};

I notice in Qt, that my designer-derived class will allocate its UI pointer onto the heap in the constructor's initializer list. Is this a good practice? If my heap-allocated objects' constructors are lean, would this make better sense?

Advertisement

It can be a good practice, and it mostly depends on the object lifetimes and behavior.

In QT's case, the pointer is back to the parent, containing object. The UI object should always have a shorter life than its parents, and it needs a pointer to send messages back up stream.

Think about those carefully. The object lifetimes of the owner will always exceed the lifetime of the child, and the child needs a way to communicate back to the parent. When both are true, the design works well.

If there were some way the parent or owner could have a shorter lifetime than the child then the design would be problematic. In that case the UI object would be holding a dead pointer which is a very bad thing; instead it should go through a handle or similar protected indirection that would consume messages to the now-missing endpoint. But since that isn't the design and object lifetimes work out, keeping a pointer to the parent works well.

Edit: Ah- misunderstood your concern. Looks like Josh Petrie handled that detail.

It's reasonable to allocate within the initializer list. It's "slightly faster" in theory, because there is only initialization of the pointer instead of initialization and reassignment (however this difference is likely nonexistent in practical applications). The performance of the constructors of the thing you're heap-allocating is irrelevant since they're executed in both cases. Obviously if you need to execute any logic prior to being able to heap-allocate something, you'll need to put that logic in the body and thus you'll need to allocate in the body (after hopefully initializing to null in the initializer list), unless you can encode that logic in the initialization of some member known to initialize before the pointer (that's a bit dangerous though).

Note however, that if you allocate in the initializer list and you want to handle exceptions that may be thrown as a result of the allocation you'll have to use a function-level try block. Since most people don't seem to know what a function-level try block is (or that it exists), usually you see people allocate in the constructor body if they intend to handle exceptions.

Note that it's legal and safe to delete a null pointer, so your "if (model) delete model" test in the destructor is not neccessary; you can just "delete model."

For the purpose of error-checking, a better alternative is, as you mention, initialize mem-pointers to nullptr in the constructor. Then implement another class function such as bool Init(). In that Init() call, you can perform required allocations and other initializations as desired. The benefit of a separate Init() call: you can return an error indication if any part of your initialization fails.

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

Your Test class is broken, because you did not follow the rule of 0/3/5. It will not work if its copied somewhere.

You are better off not using a raw pointer and not using new/delete. Just declare it as a normal member variable or if thats not possible use std::unique_ptr, which handles all difficult things automatically.

For the purpose of error-checking, a better alternative is, as you mention, initialize mem-pointers to nullptr in the constructor. Then implement another class function such as bool Init(). In that Init() call, you can perform required allocations and other initializations as desired. The benefit of a separate Init() call: you can return an error indication if any part of your initialization fails.

I'm pretty certain this goes against the RAII pattern. While i will say it can be better than a bad allocation in the constructor, i would say that this is an explicit case where exception handling is well suited.

Another option if exceptions are unwarranted is not to directly call your constructor, instead call an intermediary function which does the actual allocation/checks, and then passes that data to your actual object constructor, or reports an error if allocations failed.
Check out https://www.facebook.com/LiquidGames for some great games made by me on the Playstation Mobile market.

Also note that if the allocation fails and you have more than one heap allocation in the initializer list (and said allocation throws std::bad_alloc or you throw an exception in the constructor body), you've got yourself a nice memory leak, because the destructor will never get called.


class Test
{
    Model* model1;
    Model* model2;
public:
    Test() :
        model1(new Model),
        model2(new Model)  // this allocation fails
    {}

    ~Test()  // this will never get called
    {
        if(model1) delete model1;
        if(model2) delete model2;
    }
};

You're on the safe side if you use smart pointers.


class Test
{
    std::unique_ptr<Model> model1;
    std::unique_ptr<Model> model2;
public:
    Test() :
        model1(new Model),
        model2(new Model)
    {}

    ~Test()
    {}
};
"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty

If you're using C++11, you can just initialize to nullptr in the class body's definition.


class Test
{
private:
    Model *model = nullptr; //Initialized.

public:
    Test() //: model(nullptr) <-- Not needed
    {
        model = new Model();
    }
    ~Test()
    {
        delete model;
    }
}

Also, you may want to also consider smart pointers. A std::unique_ptr is usually the same size as a regular pointer*, and deletes itself when the class gets deleted. Depending on your performance needs, a unique_ptr for situations like this may save some programmer mistakes.

Alternatively, 'model' might not need to be heap-allocated at all depending on what you are doing.

*if using the default deleter.

=================

As far as the initialization list goes, there's a gotcha you need to be careful about:


class Something;

class Test
{
public:
    Test() : something(this) //Careful when passing the 'this' pointer in the initializer list.
    {
        something.DoSomething(); //Fine. 'something' is fully constructed.
    }
    
    void Blah()
    {
        //...
    }
    
private:
    Something something;
};

class Something
{
public:
    Something(Test *test) : test(test) //Fine, we're just storing the pointer address.
    {
        test->Blah(); //ERROR! 'Test' hasn't been fully constructed yet!
    }
    
    void DoSomething()
    {
        test->Blah(); //Fine. 'test' is fully constructed.
    }
    
private:
    Test *test = nullptr;
};

If we initialize a member in an initialization list, and pass it a pointer to ourself, we haven't yet been fully constructed yet!

During the initialization list, our memory has already been allocated and the 'this' pointer points to the right space, but that memory hasn't been initialized yet, so that pointer can't be used to read or write to our members or call functions that read or write to the members. It'd be undefined behavior. It likely won't crash, because the pointer address points to legit memory, but the memory contents are still gibberish.

As far as using the 'this' pointer goes, we can store it, but we can't predictably use it until we reach the constructor's function-body (which is after the initializer list has already been gone over).

Doing memory allocations in C++ with raw pointers is a disaster waiting to happen.

This topic is closed to new replies.

Advertisement