Skip to content

Instantly share code, notes, and snippets.

@keebus
Last active October 10, 2019 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keebus/57f1f8550276f10fb2926033f4a2e613 to your computer and use it in GitHub Desktop.
Save keebus/57f1f8550276f10fb2926033f4a2e613 to your computer and use it in GitHub Desktop.
Dynamic traits

Dynamic Traits

During ordinary C++ development we ofter encounter the situation where we want to operate with similar objects through a common interface. C++ natively supports only one idiomatic way of implementing dynamic function dispatch, the _virtual_ keyword. The idiomatic, traditional OOP way to solve this problem in C++ is to declare pure-abstract classes, also called interfaces and implement them in other abstract or concrete classes in their declaration explicitly.

This solution has at least a couple of serious drawbacks:

  1. It introduces strong type coupling between the class that implements the interface and the interface itself. Specifically, the class needs to know in advance what interfaces it wants to implement. Although this is sometimes the case, it is not always the case, and more often this coupling is undesirable. Finally, C++ classes once declared cannot later on be opened, adding more implementations to interfaces. This means that you cannot, through standard C++, declare an interface after their potential implementers.
  2. Since the interface is based on virtual member functions, all instances of the concrete class will be one pointer larger for the virtual table. Moreover, unless the implementing functions are tagged with the C++11 keyword final, every invokation of such functions through pointers or reference will be compiled as an indirect call. Again this is avoidable by marking the class or each member function implementing the interface final, but it is a C++11 feature.

Practically, imagine the following scenario. You are using a couple of third party libraries, and they both provide several container classes. You would like a common way to interoperate with these classes, without modifying the original source code, for example you would like to create a way to insert elements to instances of these containers, regardless of their actual type.

This is precisely what dynamic traits solve. They are in some sense similar to the classical way of writing a pure abstract class and implementing it for a concrete type, but they allow this process to be decoupled from both the interface and the implementations.

Here is a simple usage example: suppose you have a couple of container classes in library A named Array and Set and you would like to create a common interface to retrieve their number of contained elements.

// Declare a Sizeable trait with a pure virtual member function to retrieve the size.
class Sizeable
{
public:
  virtual size_t getSize() const;
};

// ...

// Somewhere else implement the trait for both types Array and Set by declaring a
// specialization for the TraitImpl template undefined struct.
// This type *must* (and is checked at compile time that it does) derive from the
// trait type.
template <typename T>
struct trait_impl<Sizeable, Array<T>> : Sizeable
{
  Array<T> const* mSelf; // You can here declare a "self" pointer to the Array<T> instance.
  
  // And implement the trait:
  size_t getSize() const final
  {
    return mSelf->getLength();
  }
};

Now the user can use trait_ptr<Sizeable> to get a "pointer to an object that has the Sizeable trait" like so.

void printSize(trait_ptr<Sizeable const> sizeable)
{
  std::cout << sizeable->getSize();
}

// ..
int main()
{
  Array<int> array = { 1, 2, 3 };
  printSize(&array); // prints 3
}

The cost of calling getSize is that of a virtual call with a dispatch call, unless the dispatch call is inlined (as probably in this case).

Implementation

Let's begin by forward declaring a helper type we'll use to provide trait implementations for our classes.

template <typename Trait, typename Type>
struct trait_impl;

Then the implementation of trait_ptr.

template <typename Trait>
class trait_ptr
{
	// Make sure that Trait is polymorphic and pure abstract (no member data).
	static_assert(std::is_polymorphic<Trait>::value && std::is_abstract<Trait>::value && sizeof(Trait) == sizeof(void*), "Invalid Trait, it must contain at least one virtual member function and all functions must be pure.");

public:
	trait_ptr() : m_vptr(nullptr), m_self(nullptr)
	{
	}

	trait_ptr(nullptr_t) : trait_ptr()
	{
	}

	template <typename Type>
	trait_ptr(Type* ptr)
	{
		// The trait implementation type.
		using trait_impl_t = trait_impl<std::remove_cv_t<Trait>, std::remove_cv_t<Type>>;

		// Safety checks.
		static_assert(sizeof(trait_impl_t) <= sizeof(*this), "Invalid trait implementation for type Type, it must be not larger than two pointers.");
		static_assert(std::is_base_of<Trait, trait_impl_t>::value, "Trait implementation must derive from Trait.");
		static_assert(!std::is_const<Type>::value || std::is_const<Trait>::value, "Cannot instantiate a non-const trait pointer from a const instance.");
		static_assert(!std::is_abstract<trait_impl_t>::value, "Trait implementation is abstract. Did you forget to define some of trait virtual methods?");

		// And here is the trick: this function will essentially copy the virtual table ptr to m_vptr.
		new (this) trait_impl_t{ ptr };
	}

	bool valid() const
	{
		return m_vptr != nullptr;
	}

	template <typename Type>
	trait_ptr& operator=(Type* ptr)
	{
		new (this) trait_ptr{ ptr };
		return *this;
	}

	Trait* operator->()
	{
		assert(m_vptr);
		return (Trait*)this;
	}

	Trait const* operator->() const
	{
		assert(m_vptr);
		return (Trait const*)this;
	}

private:
	void* m_vptr;
	const void* m_self;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment