Skip to content

Instantly share code, notes, and snippets.

@keebus
Last active September 28, 2021 20:49
Show Gist options
  • Save keebus/16a0ecd764f849207dd21c921d3bc6db to your computer and use it in GitHub Desktop.
Save keebus/16a0ecd764f849207dd21c921d3bc6db to your computer and use it in GitHub Desktop.
Opaque Class

Opaque Class

I believe C++ classes implement encapsulation extremely poorly as they require private variables and functions to be declared together with the public interface, clashing with C-style code modularization based on header files inclusion. To implement real incapsulation, we would like to only present the class public interface in its header, hiding all the other information. Unfortunately the canonical ways to achieve this in C++ are both flawed as they introduce unnecessary overhead.

  1. One is Pimpls for Private Implementations. They rely on forward declaring a private class that actually implements the public class functionalities and then only holding a pointer to this private class within the public class. This adds one level of indirection, as now a pointer to the public class does not point to the data iself but to a pointer to the data.
  2. Virtual interfaces are the other way, which is even worse than Pimpls as they add overhead to each member function call.

Although C does not support classes natively, it actually allows true encapsulation, as this can simply be achieved by declaring an opaque structure together with a bunch of public functions that take that struct pointer as one of their arguments. In C++ we're stuck to this solution, which limits API design as we now cannot have a piece of state with operations attached to it, just like in regular classes.

My idea of opaque class tries to work around this issue with zero overhead. Although this may look like a hack over C++ type system, its correctness is statically enforced and therefore client code remains type-safe. This approach works particularly well when creating abstraction layers over different implementations that can be switched statically (e.g. over the different graphics API).

class Texture
{
	// Mark this class opaque.
	opaque(Texture);
	
	// Creates a new texture with specified width and height.
	static Texture* Create(int width, int height);
	
	// Destroyes the texture.
	void Destroy();
	
	// Look ma'! No data!
};

Opaque classes cannot be freely instanciated by the user like so.

Texture texture; // This won't compile
Texure* texture = Texture::Create(1024, 768); // This is ok

Analogously they cannot be freely deleted by the user using the delete keyword, but only by calling the Destroy() function.

delete texture;     // This won't compile
texture->Destroy(); // This is ok

Now imagine we want to provide a D3D11 implementation for such opaque class. In a TextureD3D11.cpp file we write.

#include "RenderTarget.h"

// Every opaque class declare an inner Impl structure that is meant to be defined in the associated cpp file with
// the actual implementation.
opaque_impl(Texture)
{
	// Add all the API specific member data and private member functions here.
	ID3D11Texture2D* mTexture;

	void Initialize()
	{
    		...
  	}
	
	void Terminate()
	{
		...
	}
};

// And implement Texture public functions
Texture* Texture::Create(int width, int height)
{
	Impl* texture = new Impl;
  
	// Create the actual D3D11 texture.
	texture->Initialize();
	
	...
	// Return the implementation texture as a Texture.
	return texture;
}

void Texture::Destroy()
{
	// Every opaque class also automatically defines a `self` member variable that only functions
	// as convenient accessor to the implementation.
	self->Terminate();	
	delete self.ptr();
}

...

This approach not only provides true encapsulation for complex or platform-specific classes, but it also greatly improves compilation time as most of the code is moved from the header to the cpp.

Implementation details

The implementation for opaque is actually quite simple and relies on forward declaration.

#define opaque(Class)\
	private:\
		friend OpaqueImpl<Class>;\
		Class() {}\
		~Class() {}\
		struct Impl;\
		OpaqueSelf<Impl> self;\
	public:
	
#define opaque_impl(Class)\
	struct Class::Impl : OpaqueImpl<Class>

While OpaqueImpl is mainly used for type checking.

template <typename OpaqueClass>
class OpaqueImpl : public OpaqueClass
{
	static_assert(sizeof(OpaqueClass) <= 1, "Opaque class cannot have member variables or virtual functions.");
private:
	friend typename OpaqueClass::Impl;
	OpaqueImpl() {}
};

And OpaqueSelf is a convenient type only used to access the implementation and ease debugging since it is paired with a NatVis description that expands it to the Impl instance.

template <typename Impl>
struct OpaqueSelf
{
	Impl* 		ptr() { return (Impl*)this; }
	Impl const* 	ptr() const { return (Impl*)this; }
	Impl& 		operator*() { return *ptr(); }
	Impl const& 	operator*() const { return *ptr(); }
	Impl* 		operator->() { return ptr(); }
	Impl const* 	operator->() const { return ptr(); }
};

And the NatVis

<?xml version="1.0" encoding="utf-8"?>  
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">  
	<Type Name="OpaqueSelf&lt;*&gt;">
		<Expand>
			<ExpandedItem>*($T1*)this</ExpandedItem>
		</Expand>
	</Type>
</AutoVisualizer>  
@nyanpasu64
Copy link

Shame this was never adopted... Why can't you delete an opaque type? Does the regular delete operator not know the size of the class? Perhaps you could instead override the delete operator to call a C-style destroy free-function instead (I didn't test that it works): https://stackoverflow.com/questions/14819760/override-delete-operator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment