Using template-based providers for component modularity.

c_plus_plus

I write seldom about c++ techniques. First of all because I find these kind of posts rather dry, then there’s plenty of people much more skilled than me in that area and finally, it’s always bound to endless discussions about the ‘proper’ way of doing things.

However, in this case, I’ll do a small exception. Taking advantage of my #elternzeit, I’ve got some slices of time on my hand to freely dive into my own code and experiment new techniques – both from the user perspective and code writing.

Rewriting my Raspberry Pi/Launchpad sequencer, I needed to re-organize some code and came to a coding method that felt like a mini-breakthrough for me so I feel like sharing it.

Overview

Let’s consider the goal I was trying to achieve: my current focus is to write software but in a sense that it doesn’t feel like using computers. I rely on driving raspberry Pi’s solely from controllers so that the Pie itself becomes somehow invisible and only the interface(s) matters. As such, I’ve been doing quite a bit of work using the novation launchpads as they are cheap, reliable and provide a wide array of led to play with.

With that in mind, I decided to think of the launchpad as a ui with a 8×8 pixel interface display: the grid is my window, on which I can display different types of views, that can contain several parts that act like sub-views or widgets.

Let’s consider these two pictures, each representing a view:

IMG_20150826_150826 (1)         IMG_20150826_150859

The first one is a grid keyboard layout, as introduced by Push.  In this particular example, it allows to play a synths that also runs on the raspberry pi. Most of the space is dedicated to playing notes, but the top row can be used to trigger chords, each of the buttons triggering the nth chord of the current scale, using inversion for proper transition.

The second one is the scale selection screen, following the system developed by motscousus on the launchpad95 script for live. One switches temporarely to the scale selection view pressing the top button from the right row.

Separating each of the two views into sub-views has two advantages. The first one is that subviews are easier, simpler classes to deal with. The second one is that they can be reused in different contexts. The most obvious element to be re-used would be the grid keyboard  part. One might want to extend it to the whole grid, or to use it as element for inputing notes into a sequencer, etc.. Of course we don’t want to re-implement its logic every time.

Class hierarchy

In terms of classes, we have something like the following structure:

Screen Shot 2015-08-27 at 09.45.59 Nothing mind blowing about it but we want to achieve two goals:

  1. We want to be able to freely and painlessly make new views, and therefore (re)using subviews
  2. We want the client to change (synth/sequencer/refactor) and therefore establish a loose coupling between the subviews and the client so that writing new client isn’t a pain in the butt.

Interaction

Now we come to the interesting part of the problem. In order to function properly, the subviews need to work on some data and trigger actions. There are a couple of ways of doing this, but most of them have pitfalls:

Driving from the outside

One way would be to add getter/setters for whatever the subview needs to operate properly. For example, the Gridkeyboard can be set display notes in chromatic mode (where all notes are shown but only the ones from the scale are highlighted) or in ‘push’ mode (where only the notes in the scale are shown). We could implement

void GridKeyboard::ShowChromatic(bool chromatic);

but that would mean that every time we compose the subview into a new view, we’d need to implement some forwarding mechanism at the view level

void KeyboardAndChordView::ShowChromatic(bool chromatic)
{
  keyboardSubView_.ShowChromatic(chromatic);
}

or add an accessor to the subview so that the client can trigger it directly

GridKeyboard& KeyboardAndChordView::GetGridKeyboard()
{
  return keyboardSubView;
}

Furthermore, the GridKeyboard also needs to communicate to the client when notes are pressed so that appropriate actions can be taken. In this context, the ‘standard’ method would be to setup some notifying/messaging/listener system the client can attach to so it can listen to messages from the subviews and process them accordingly. Then again, the fact subviews are composed into views means there is extra work to relay the messaging from the bottom level of the subview to the client through the view. Also, even though listeners are a fairly understood system, having a decent, evolutive and clear notification system is actually hard to design and too much notifiers can dangerously entangle your application logic.

All in all, this means that using this method, composing subviews will always lead to a fair bit of plumbing/wiring at the view level, in the sole purpose of connecting the subviews and the client.

Provider Interfaces

The next method would be to use some kind of providers that represents the interface needed by the subview to work properly. In the case of our GridKeyboard, we would do something like:

class GridKeyboard
  : public SubView
{
public:
  class IProvider
  {
    virtual void ChromaticMode() = 0;
    virtual void ProcessNote(const NoteEvent& event) = 0;
    ...
  };

  GridKeyboard(GridKeyboard::IProvider& provider)
    : provider_(provider)
  {
  }

private:
  IProvider& provider_;
};

The benefit of this is that composing the elements into a view requires a lot less work since all the view needs to do is to take a  GridKeyboard::IProvider at construction time and forward it to the GridKeyboard

class KeyboardAndChordView
{
  KeyboardAndChordView(
    GridKeyboard::IProvider& gridKeyboardProvider,
    ChordSubView::IProvider& chordProvider)
    : gridKeyboardProvider_(gridKeyboardProvider),
      chordProvider_(chordProvider)
  {
  }

  NBase::Result CreateLayout() override
  {
    addSubView<ChordSubDisplay>(SubView::Rect(0, 0, 8, 1), chordProvider_);
    addSubView<GridKeyboard>(SubView::Rect(0, 1, 8, 7), gridKeyboardProvider_);
    return NBase::Result::NoError;
  }
  .. 
}

Now, as long as our client can provide support for the defined interfaces, all is wonderful. We’ve also removed the need for messaging and replaced it with an API which is going to be less cumbersome to deal with.

But, things aren’t always as easy as we would like it to.

First of all, let’s look at the case of the GridKeyboard subview and the ScaleSelection subview. Although they will most likely belong to distinct views, both will need to access the current octave setting (the first one to be able to send the correct midi event, the second one to display the octave setting). This means that both GridKeyboard::IProvider and ScaleSelector::IProvider will contain a virtual method

virtual void int8_t Octave() = 0;

If the client wants to use both, it will be confronted to implementing two separate object to implement each provider as we can’t easily derive from two interfaces having common signatures. This means that, once again, we’re forced to implement some plumbing, this time at the client level, to dispatch the source data for the octave setting through the different provider interfaces. I also becomes clear that when the number of subviews grows, the amount of work to setup the different providers will become a hassle.

Provider templates

So, in the end, what we would like is to be able to use a provider for the subviews, but without having to deal with setting up interfaces. As long as the provider supplies a method that has the right signature i.e.

void int8_t Octave();

it should be sufficient to supply the api required by the subview. To do this, we’ll use a template base provider

template <class Provider_t>
class GridKeyboard
  : public SubView
{
public:
  GridKeyboard(GridKeyboard::IProvider& provider)
    : provider_(provider)
  {
  }

private:
  Provider_t& provider_;
};

With this approach, we don’t need to define any specific upfront requirement for the provider. Whenever the template will be compiled, if the supplied Provider_t doesn’t mach a method needed by the subview, it simply wouldn’t compile. Note also that doing this, we got rid of some boiler plate for the IProvider class.

Combining the subviews into a view gets rather straightforward too as there is no further need on that level than forwarding the Provider_t type to the view that gets created.

 template <class Provider_t>
 class KeyboardAndChordView
 : public View
 {
 public:

 KeyboardAndChordView(Provider_t& provider)
 : provider_(provider)
 {
 }

 NBase::Result CreateLayout() override
 {
   addSubView<ChordSubDisplay<Provider_t>>(SubView::Rect(0, 0, 8, 1), provider_);
   addSubView<GridKeyboard<Provider_t> >(SubView::Rect(0, 1, 8, 7), provider_);
   return NBase::Result::NoError;
 }

And now, the client has the full choice to implement the API needed by both subviews. Any method needed by multiple subview needs to be implemented only once. All in all, everything is greatly simplified and much more modular.

Side Effects

However, like always in programming, nothing is done with side effect and it is important to know what are the consequences of a given choice.

  1. Faster execution: compared to using interfaces with virtual method, using template will call directly any method on the provider, rather than having to access the method address through a virtual table. In our case it probably doesn’t change things enormously as the view code isn’t very complex and is not intended to be run at very high speed, but one might think of other situations (running audio processing code for example) where this could lead to a subsequent performance improvement
  2. Less obvious interface: since there is no explicit listing of the method needed by the components, it might be a little hairy to understand how the component works and what is expected from the provider template argument. Sure, your code won’t compile if you don’t comply to the component’s needs, but if you are looking to know upfront what kind of service you need to provide and whether a given architecture would work in the grand scheme of things, templetization this way makes planning and design a tad bit complicated. C++ should introduce the ability to define concepts which help to define the interface a given template has to comply to (therefore making the needs of the component more obvious) but at this point, there is nothing solid, cross-platform to support it. This is where providing examples or unit tests for components can be key as a method of explaining what it does and how it is supposed to work.
  3. Compilation time: whenever something needs to be done, usually there is no magical way of cutting corners on the amount of work needed. If you do less, the compiler will have to do more. Using template-based classes inevitably means more work for the compiler and therefore longer compilation time. This can become VERY substantial when your project grows. Moreover, template based class are purely header based so whenever you touch any part of their implementation, you will need to recompile ALL code that includes it. This means that, even more when depending on template-based components, you should be aware of component dependencies and relationship. If anytime you change a line, you need to wait half an hour of compilation, you won’t be effective at producing anything to users.
  4. More complex errors: Although in our case the errors reported will most likely about the provider not implementing correctly a signature needed by the component, when you start using templates heavily and combine components, the errors spat by the compiler can become extremely cryptic. This is most likely fine if you work in your own code, but can be extremely puzzling for someone that will use some component you wrote. You might need half a day just to figure out what the compiler complains about.

Huge thanks to @JackSchaedler for his help on getting this text in good shape.

1 comments

  1. I’d love to hear more about your Raspberry Pi/Launchpad sequencer. I’m trying to make a controller with this combo at the moment.

Leave a Reply

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