Make your C++ interfaces ‘trait’-forward

Having tried the Rust language recently, I was glad to see the lack of typical object-oriented concepts in favour of a greater commitment to parametric polymorphism (a.k.a. generic programming). Its means of doing so – traits – is subtly different to what most programmers are used to, yet some of their advantages can be implemented right now in your C++ code. Applying their principles will help you define flexible interfaces, avoid cryptic template error messages, and adapt legacy code with ease.

As OOP goes through its final death, what will the prevailing way of modelling our software look like? It certainly looks like we are converging towards functional programming, with the addition of lambdas in nearly every language but C and many modern languages supporting type inference and/or generics. Some good efforts have been made with declarative programming, the most common example in use today being SQL but C# has made a heroic effort with LINQ. But there will always be a need for algorithms that require a certain set of permitted operations on the data types they use.

Most familiar languages (Java, C#) achieve this with interfaces, which define a set of functions required to achieve some goal or property of an object. For example, IList is an interface that lets you add, get and remove objects contained within another object. The author of the type must specify the interfaces supported by that type. If the type comes from a library, for example, you can’t make that type implement an interface you have created without resorting to the Adapter pattern.

interface IList
{
    Object getObject(int index);
    void setObject(int index, Object newValue);
}

class MyFancyArray : IList
{
    Object getObject(int index) {
        // code to get an object at the given index
    }
    void setObject(int index, Object newValue) {
        // ...
    }
}

Interestingly, Go has a few features that makes this easier – anonymous fields and implicit interfaces – but you still have to define a new struct to implement the interface for an existing type, then wrap instances of that existing type in your new interface-implementing type. This may sound like a palaver, but in the absence of generics, the worst problem you can have is verbosity.

Haskell has a different concept of typeclasses. A typeclass is also a set of functions required to implement a particular concept. However, Haskell types are not ‘objects’ and do not come with ‘methods’, so typeclasses have to be defined separately from the type. Importantly, this means anyone can define a typeclass and make it support any type, even if you didn’t write it or it comes from a library.

class Eq a where
  (==), (/=)            :: a -> a -> Bool
  x /= y                =  not (x == y)

instance Eq Foo where
   (Foo x1 y1) == (Foo x2 y2) = (x1 == x2) && (y1 == y2)

Rust’s traits are very similar to typeclasses. They are the primary method of dynamic dispatch as the language, like Haskell, does not support inheritance on its record types. But the value they bring to generic functions using static dispatch can be applied to C++ using templates, without sacrificing the convention of implicitly typed interfaces. Let’s see an example.

template <typename T>
class GraphicalTrait {
public:
  GraphicalTrait(T& self_) : self (self_) {}
  int getWidth() const { return self.getWidth(); }
  void setWidth(int width) { self.setWidth(width); }
  void render() { self.render(); }
private:
  T& self;
};

This trait is an abstraction of a graphical item, defining three methods. The default implementation proxies the call to the referenced object – the aforementioned implicit interface – but by specializing the template we can implement the trait for a type that does not have the getWidth etc. methods.

template <>
class GraphicalTrait<BigBallOfMud> {
public:
  GraphicalTrait(BigBallOfMud& self_) : self (self_) {}
  int getWidth() const { return self.GetWidgetWidthEx(); }
  void setWidth(int width) { self.SetWidgetSizeNew(width, self.GetWidgetHeightEx()); }
  void render() { cout << self.WidgetTextRepresentation << endl; }
private:
  BigBallOfMud& self;
};

The general concept is more important for legacy code or adapting libraries; new code can be written that does not require a major upheaval of existing code, allowing your code base to be refactored gradually.

What benefits does this give us over typical implicit interfaces in C++? Documentation and API usage is easier, because defining the trait interface forces you to enumerate all of the operations you are expected to implement. Writing code using the trait is easier, as it makes code completion possible. And code comprehension is improved because it is easier to reason over a smaller interface (that which defines the trait) than over a larger one (a whole class).

How do we write code that uses GraphicalTrait? Using templates, of course, but the devil’s in the details:

Option 1: Accept any type as a parameter, and create an instance of GraphicalTrait in the function body. This is easiest for users of your function, as they can directly pass an instance of their type implementing the trait you are using. However, like most uses of templates in C++, the function signature does not enforce the interface the type is expected to implement.

template <typename GraphicalTraitType>
void embiggen(GraphicalTraitType& graphicalObject) {
  GraphicalTrait<GraphicalTraitType> graphicalTrait(graphicalObject);
  graphicalTrait.setWidth(graphicalTrait.getWidth() * 2);
}

The process is easier if you have a class where the trait reference is a member; in this case, you can initialise the member in the constructor and there is no more code than if it were a standard reference type.

Option 2: Accept a GraphicalTrait as a parameter, callers construct a GraphicalTrait. This makes the interface explicit but callers are forced to specify boilerplate, as C++ template deduction rules do not take into account implicit conversions.

template <typename T>
void embiggenedRender(GraphicalTrait<T> graphicalObject) {
  auto oldWidth = graphicalObject.getWidth();
  graphicalObject.setWidth(oldWidth * 2);
  graphicalObject.render();
  graphicalObject.setWidth(oldWidth);
}

embiggenedRenderer(GraphicalTrait<CBWidget> (myCBWidget));

People with an interest in the drafting of C++11 may have heard of concepts; what I just described is concept maps. It is a shame that they never made it into the language; it is a convenient way of adapting existing code, and without the syntactic sugar offered by a language like Rust, the result will always be a little clunky for the code producer or consumer. On the other hand, this clunkiness is no different to most other C++ features!

Leave a Reply

Your email address will not be published. Required fields are marked *