Jump to content
  • Advertisement
Sign in to follow this  
Batzer

Object initialization design issue

This topic is 2028 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

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?

Share this post


Link to post
Share on other sites
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)
    {
    }
};

Share this post


Link to post
Share on other sites

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?

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites
What's wrong with your example code?
Renderer::Renderer()
 : device(CreateDevice())
 , cbuffer(device)
{}

Share this post


Link to post
Share on other sites

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.

Edited by Aardvajk

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites


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

Share this post


Link to post
Share on other sites


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.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!