The Concept of Immutability and Descriptor

Warning! Some information on this page is older than 6 years now. I keep it for reference, but it probably doesn't reflect my current knowledge and beliefs.

Fri
27
Nov 2009

An object of some class represents a piece of data, chunk of memory or other resource along with methods to operate on it. It should also automatically free these resources in destructor. But how should modifying these data look like? There are two possible approaches. As an example, let's consider a fictional class to encapsulate Direct3D 9 Vertex Declaration (I'll show my real one in some future blog entry). A Vertex Declaration is an array of D3DVERTEXELEMENT9 structures, which can be used to create IDirect3DVertexDeclaration9 object. First solution is to define class interface in a way that data inside can be modified at any time.

class MyMutableVertexDecl
{
public:
  // Creates an empty declaration.
  MyMutableVertexDecl();
  // Frees all allocated resources.
  ~MyMutableVertexDecl();
  
  // Copies data from another object
  void CopyFrom(const MyMutableVertexDecl &src);
  // Deletes all internal data so object becomes empty again.
  void Clear();
  bool IsEmpty() const;
  
  // I/O (Serialization)
  void SaveToStream(IStream &s) const;
  void LoadFromStream(IStream &s);
  
  // Reading of underlying data
  size_t GetElemCount() const;
  const D3DVERTEXELEMENT9 & GetElem(size_t index) const;
  
  // Modification of underlying array
  void SetElem(const D3DVERTEXELEMENT9 &elem, size_t index);
  void AddElem(const D3DVERTEXELEMENT9 &elem);
  void InsertElem(const D3DVERTEXELEMENT9 &elem, size_t index);
  void RemoveElem(size_t index);
  
  IDirect3DVertexDeclaration9 * GetD3dDecl() const;
  
private:
  std::vector<D3DVERTEXELEMENT9> m_Elems;
  IDirect3DVertexDeclaration9 *m_D3dDecl;
  ...
};

This approach seems very nice as you can create your object any time you wish and fill it with data later, as well as change this data whenever you need to. But at the same time, a question emerges: when to (re)create "destination" IDirect3DVertexDeclaration9 from the "source" D3DVERTEXELEMENT9 array? Each time the array is modified? Or maybe each time the IDirect3DVertexDeclaration9 is retrieved? Optimal solution for the interface above would be to do lazy evaluation, that is to recreate IDirect3DVertexDeclaration9 whenever it is retrieved for the first time since last time the D3DVERTEXELEMENT9 array have been modified. But...

There is another approach possible. That's the way Direct3D natively does it. Imagine we still want to write a class with internal data same as in the previous code, but this time we assume the object is immutable - its content cannot be changed after creation. So it must be initialized with full and correct data once and for all. This initialization can be done by the constructor or the separate Init method (which is a different design problem BTW). Now the new class looks like this:

class MyImmutableVetexDecl
{
public:
  // Initializes as empty declaration
  MyImmutableVetexDecl();
  // Initializes with data from other object (copy constructor)
  MyImmutableVetexDecl(const MyImmutableVertexDecl &src);
  // Initializes with given array
  MyImmutableVertexDecl(const D3DVERTEXELEMENT9 *elems, size_t elemCount);
  // Loads from a stream (deserialization)
  MyImmutableVertexDecl(IStream &s);
  
  // Serialization to a stream
  void SaveToStream(IStream &s) const;
  
  // Reading array data
  bool IsEmpty() const;
  size_t GetElemCount() const;
  const D3DVERTEXELEMENT9 & GetElem(size_t index) const;
  
  IDirect3DVertexDeclaration9 * GetD3dDecl() const;

private:
  std::vector<D3DVERTEXELEMENT9> m_Elems;
  IDirect3DVertexDeclaration9 *m_D3dDecl;
  ...
};

This approach has many adventages:

There are also some disadventages though:

Despite that, I currently believe the second (immutable) approach is usually better as it helps ensuring both better code clarity and run-time performance. It looks like the whole programming world is biasing towards it. For example, Direct3D 10 have some new immutable interfaces instead of numerous independent device states. In Direct3D there are flags for IDirect3DDevice9::SetRenderState method like D3DRS_ZENABLE, D3DRS_ZFUNC, D3DRS_STENCILENABLE etc., while in D3D10 there is a ID3D10DepthStencilState interface. You have to fill the fields of D3D10_DEPTH_STENCIL_DESC descriptor structure, pass it to ID3D10Device::CreateDepthStencilState method and then you get an immutable, single object of ID3D10DepthStencilState type, which can be activated with the ID3D10Device::OMSetDepthStencilState function.

Another example is a string class. Most string implementations for C++ (including std::string from STL) are mutable, so you can set, add and remove characters any time you want. On the other hand, string type in C# is immutable, so strings cannot be changed after created. Here is why I think the .NET approach is better:

A term related to immutable objects is a descriptor. PhysX use it extensively and that's one of reasons why I like its API so much. An object of almost every class is initialized with data from a descriptor during creation. For example:

It may look sophisticated, but for me an API designed like this enables all three most important characteristics of a library at the same time: flexibility, performance and simple interface. The "pipeline" described above has distinct stages with clear meaning, so it's easy to deduce that you can cook your convex mesh in a preprocessing stage or use single NxConvexMesh to create multiple NxActor-s.

Comments | #software engineering #directx #.net #physics #c++ Share

Comments

[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2025