Object initialization design issue

Started by
20 comments, last by Hodgman 10 years, 1 month ago

So I have been encountering a number of annoying design decesions, because I want to initialize my objects with the constructor.

Sounds weird but let me explain: Say I have a class A and class B. Class B manages multiple instances of A, but A depends on an initialized object of B.

Here is a concrete simplified example from my code:


class Renderer {
private:
    ID3D11Device* device;
    ConstantBuffer cbuffer;
}

class ConstantBuffer {
public:
    ConstantBuffer(ID3D11Device* device);
}

So the issue in this case is that cbuffer needs an initialized ID3D11Device in its constructor. I hope the problem is kind of clear now.

How do you get around this issue without using pointers? Or is this just a design flaw on my side?

Advertisement
class DeviceWrapper
{
public:
    DeviceWrapper()
    {
        // Initialize MyDevice here, according to RAII principles
    }

    ~DeviceWrapper()
    {
        // RAII again: clean up MyDevice
    }

private:
    ID3D11Device* MyDevice;
};

class ConstantBuffer
{
public:
    ConstantBuffer(DeviceWrapper& wrapper)
        : MyDeviceWrapper(wrapper)
    {
    }

private:
    DeviceWrapper& MyDeviceWrapper;
};

class Renderer
{
private:
    DeviceWrapper MyWrappedDevice;         // IMPORTANT - must be first (order of construction is crucial)

    ConstantBuffer MyConstBuffer;

public:
    Renderer()
        : MyConstBuffer(MyWrappedDevice)
    {
    }
};

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

Thanks for your fast reply!

So basically making a wrapper class is a valid solution for this kind of problem.

I kind of get the impression that the body of a constructor is useless in C++ since it seems to be best practice to initialize everything in the initializer list.

Is using a pointer (or std::unique_ptr) in this case considered bad practice?

Constructor bodies are still useful in some cases where you need to perform additional logic beyond just constructing members. For instance, the constructor of the DeviceWrapper is the perfect place to do your D3D device initialization.

A pointer (or smart pointer) is totally legitimate if you want it to be nullable and/or portray some specific ownership semantics. I prefer references when possible, since they are a little less error-prone, but nobody is going to hang you by your entrails for using a pointer to a component that you need access to - especially if the lifetime/ownership semantics of that component are nontrivial.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

What's wrong with your example code?
Renderer::Renderer()
 : device(CreateDevice())
 , cbuffer(device)
{}

Something I've gradually started to come to prefer over the years is to not actually bind, in this example, the device to the renderer but to just pass the renderer a reference to the device in every method that needs to use it. Renderer an odd example, but a more realistic example, a Texture, I used to do:


class Texture
{
public:
    Texture(GraphicsDevice &graphics);

    void load(const std::string &path);

private:
    GraphicsDevice *graphics;
};

I've come to prefer:


class Texture
{
public:
    Texture();

    void load(GraphicsDevice &graphics, const std::string &path);
};

I'd be hard pressed to say exactly why I find this easier to deal with and maintain if I'm honest but over the course of an entire system, it feels cleaner to me. Since I can't actually identify clear reasons, this may be me being superstitious.

EDIT: Suppose one thing I like is I'm not hiding dependancies. I.e. with approach 1, if I pass a Texture around I am silently and invisibly passing a GraphicsDevice around as well. With the second approach, this is explicit and so easier to track where the GraphicsDevice object is moving to. Bit moot if the Texture interface never exposes the GraphicsDevice though to be fair.

In your case, you might want to consider NOT requiring ConstantBuffer to have an initialized device in the constructor. I would then defer the initialization to "init" methods. Something like:


// headers

class Renderer {
private:
    ID3D11Device* device;
    ConstantBuffer cbuffer;
public:
    bool init();
}
 
class ConstantBuffer {
public:
    ConstantBuffer();
    bool init(ID3D11Device* device);
}

// cpps
void Renderer::init() {
 // inialize ID3D11Device
 // ...
 cbuffer.init( device );
 // ...
}

void ConstantBuffer::init() {
  // ...
}
 

This gives you some flexibility when dealing with errors, for example. The only way a constructor can notify errors is by throwing exceptions, but you may not want to use them (either now or a in a future port). Having a init method let you fail via exceptions or via returning error values.

Having a separate init method also let you re-initialization, which you may want in the future.

This design pattern, by the way, is called Two-Phase Construction: http://developer.nokia.com/community/wiki/Two-phase_construction.

“We should forget about small efficiencies, say about 97% of the time; premature optimization is the root of all evil” - Donald E. Knuth, Structured Programming with go to Statements

"First you learn the value of abstraction, then you learn the cost of abstraction, then you're ready to engineer" - Ken Beck, Twitter

The only way a constructor can notify errors is by throwing exceptions, but you may not want to use them (either now or a in a future port). Having a init method let you fail via exceptions or via returning error values.

You can return an error value from a constructor through an out-parameter.

The catch with two-phase construction is that it requires your objects to have a "zombie state", where they're constructed but not yet initialized. For some classes that's easy, like an empty std::vector, or a null cbuffer, but for other classes it's sometimes ugly. The client also needs to be aware of this design (knowing that it's possible for a valid cbuffer pointer to internally be null), so they probably need an IsInitialized() function, etc.

Zombie states are also a 3rd option for reporting errors out of a constructor - you require the user to call IsInitialized() immediately afterwards.

Personally, I just like to make it so constructors cannot fail, so I have a CreateDevice function which makes an ID3D11Device, and then constructs my own device class, passing in the D3D object as a parameter.


You can return an error value from a constructor through an out-parameter.

Emmm, yep, I completely forgot about that one. The problem is that you still end up with a messed up object resulting from calling the constructor, but it is in a permanent uninitialized state. With two-phase construction at least you can go from uninitialized to initialized via the init method :)

Regadless, I am with you that the best constructor is one which simply cannot fail :) I like your method if encapsulating Direct3D in the API is not the point of Renderer and ConstantBuffer...

“We should forget about small efficiencies, say about 97% of the time; premature optimization is the root of all evil” - Donald E. Knuth, Structured Programming with go to Statements

"First you learn the value of abstraction, then you learn the cost of abstraction, then you're ready to engineer" - Ken Beck, Twitter


I'd be hard pressed to say exactly why I find this easier to deal with and maintain if I'm honest but over the course of an entire system, it feels cleaner to me.
I don't know Aardvajk, I'm not quite on it. The main problem I have here is that you cannot take a Texture object and poll which device created it (maybe we can pull out the creation device from the OS handle, but in isolation, that's still not enough). This information will have to be tracked with surrounding code. It's not like I'm against this idea in itself but I always considered statements like "parameter X must be creator" a last resort. If I am allowed to specify a different object "for every method", then I'd intuitively feel entitled in calling load on a device and bind on multiple.

Previously "Krohm"

This topic is closed to new replies.

Advertisement