Object initialization design issue

Started by
20 comments, last by Hodgman 10 years ago

... #1, #2 and #3 ...

Interesting stuff.

My take on those 3 designs...

#1 could be viewed as a closure/partial-application of the device into the Bind() call, in a sense the Texture class is just a closure type. The DrawThing function should know that if it accepts closure types as its arguments then it doesn't get to dictate the closed environment. Either it should not have accepted closure types or it needs a way to assert a pre-condition that the two textures refer to the same device.

#2 makes me feel much more uncomfortable that #3, but I would have a hard time saying exactly why. Maybe it's just a matter of convention. Like Krohm, I would feel somewhat entitled to construct with one device and bind to another - until reading the documentation.

#3 technically suffers the same problem as #2 but follows a more familiar convention so I think it really is better than #2. It also exposes an underlying truth that the device acts like a factory for textures, whereas I think #2 kind of gives the impression that a device is merely a component of a texture.

Designs #2 and #3 try to pretend that a texture is a stand-alone object and that it can be separated from the device at one point (at creation) and re-join with it later on (at bind-time). This is almost true and close-enough that it can be implemented that way provided there are sufficient runtime checks at the re-join point.

Design #1 never lets the device separate from the texture, so the only thing it does is try to present a texture as a stand-alone object.

Textures being stand-alone raises the question of whether a texture might be able to out-live the device.

#2 and #3 don't really deal with that too well - the destruction of the device object may put the texture objects into a weird state (another example of spooky action at a distance).

Design #1 could deal with that if it used a shared_ptr to reference the device which guarantees that the lifetime of the device is at least as long as the texture's.

I usually think all of this comes down to limitations in the language itself, that you have to enforce this kind of thing yourself and there aren't great ways to describe the relationships to the compiler or to the runtime environment.

Other areas of the language have the same problem, for example you wouldn't expect to be able to use an iterator in a different container to the one that created it, or to use an iterator after the container has been destructed.

Maybe using handles (like integer handles) rather than objects would side-step some of those issues.

Advertisement

#1 could be viewed as a closure/partial-application of the device into the Bind() call, in a sense the Texture class is just a closure type. The DrawThing function should know that if it accepts closure types as its arguments then it doesn't get to dictate the closed environment. Either it should not have accepted closure types or it needs a way to assert a pre-condition that the two textures refer to the same device.

Yeah, with #1 you could add in error checking such as below... However, I'd prefer the error checking example that I made for #3, because that was internal to the system, as opposed to being the client's responsibility.


void DrawThing( Texture& diffuse, Texture& normals )
{
  myASSERT( diffuse.Device() == &device );
  myASSERT( normals.Device() == &device );
  diffuse.Bind(0);
  normals.Bind(1);
  device.DrawQuad();//The textures were bound to this device, otherwise we would've crashed above
}

Textures being stand-alone raises the question of whether a texture might be able to out-live the device.

If you're using a managed language, then the texture would probably have a reference to the device, so that the device can't be garbage collected while someone's still using it's texture.
If you're using a C-style resource management, then you should be used to dealing with specifications/rules for object lifetimes. It's a common theme that poor lifetime management will lead to bugs with horrible symptoms, so you get used to this kind of planning and engineering pretty quickly wink.png
If you're using "modern C++", then you could either go the "managed" style route above, where a texture contains a (strong) reference to the device, so the device cannot be deleted while someone still holds onto it's resources... or you could instead only hand out weak-references of resources to the client, so that when the device is destroyed, all the user's references are invalidated automagically.

Personally, I find the C-style of resource management to actually be superior to the other two, but many would disagree with me tongue.png
Raw pointers are very common in my engine, but don't pose problem due to well defined lifetime rules and good resource allocation practices.
There's a lot of cases where if you hung onto a pointer to some object/resource after it's actual owner had expired, you would get horrible bugs, but that doesn't happen by design.

Maybe using handles (like integer handles) rather than objects would side-step some of those issues.

I actually do this. I don't have a texture/buffer/etc struct for the client to use. I just make new integer types to represent them:


template<class T,class Name>struct PrimitiveWrap
{
	PrimitiveWrap() : value() {}
	explicit PrimitiveWrap( T v ) : value(v) {}
	operator const T&() const { return value; }
	operator       T&()       { return value; }
private:T value;
};
#define TYPEDEF_ID( name )				\
	struct tag_##name;				\
	typedef PrimitiveWrap<u32,tag_##name> name;	//
...
TYPEDEF_ID( TextureId );
...
TextureId tex = device.CreateTexture(...);

If you wanted to assert which device owned which resource, you could pack the device ID into some of the bits of that integer, along with the actual resource index.

This topic is closed to new replies.

Advertisement