Blog: September 2014

C++ Trait Mechanics — Part 1

| Stefanos | Comments 2

In this post, I am going to demonstrate, how you can use C++ traits to produce more robust, maintainable and optimized code. Recently I inherited a legacy code base responsible for reading and delegating signals from various hardware buttons. After the signals were read, their values were used to update LEDs and inform other ECUs about the state of the buttons. However, we noticed that for such a limited functionality the code was consuming quite a lot of ROM memory and CPU time. Our benchmarks revealed that the code below was executed every 10 ms and was written in plain C. The read functionality was implemented like this:

void C_Implementation::readButton(unsigned const id)
{
    switch (id)
    {
    case BUTTON_ID_1:
        _buttonOne._signalX = networkBus::getButtonOneSignalX();
        _buttonOne._signalY = networkBus::getButtonOneSignalY();
        _buttonOne._signalZ = networkBus::getButtonOneSignalZ();
        return;
    case BUTTON_ID_2:
        _buttonTwo._signalX = networkBus::getButtonTwoSignalX();
        _buttonTwo._signalY = networkBus::getButtonTwoSignalY();
        _buttonTwo._signalZ = networkBus::getButtonTwoSignalZ();
        return;
    case BUTTON_ID_3:
        _buttonThree._signalX = networkBus::getButtonThreeSignalX();
        _buttonThree._signalY = networkBus::getButtonThreeSignalY();
        _buttonThree._signalZ = networkBus::getButtonThreeSignalZ();
        return;
    default:
        _buttonFour._signalX = networkBus::getButtonFourSignalX();
        _buttonFour._signalY = networkBus::getButtonFourSignalY();
    }
}

Figure 1.

Where _buttonOne etc. are of following types:

struct ButtonTypeOne {
    unsigned char _signalX;
    unsigned short _signalY;
    unsigned char _signalZ;
};

struct ButtonTypeTwo{
    unsigned char _signalX;
    unsigned char _signalY;
    bool _signalZ;
};

struct ButtonTypeThree{
    unsigned short _signalX;
    signed char _signalY;
};

Figure 2.

Here we found our main bottleneck: We had about 15 such functions in the code, all differentiating between the buttons via switch/if/else statements. In addition to the performance problem, the code itself was not really maintainable. Adding or removing a single button requires changing all these functions, which is both tedious and error prone. This is also a violation of the open closed principle. Finally let’s assume for this blog’s purpose that the output functionality looked like this:

void C_Implementation::updateButtonLeds(unsigned const id)
{
    switch (id)
    {
    case BUTTON_ID_1:
        std::cout << "Button one output :\n"
            << "Signal X is : " << _buttonOne._signalX << "\n"
            << "Signal Y is : " << _buttonOne._signalY << "\n"
            << "Signal Z is : " << _buttonOne._signalZ << "\n";
        return;
    case BUTTON_ID_2:
        std::cout << "Button two output :\n"
            << "Signal X is : " << _buttonTwo._signalX << "\n"
            << "Signal Y is : " << _buttonTwo._signalY << "\n"
            << "Signal Z is : " << _buttonTwo._signalZ + _buttonTwo._signalY<< "\n";
        return;
    case BUTTON_ID_3:
        std::cout << "Button three output :\n"
            << "Signal X is : " << _buttonThree._signalX << "\n"
            << "Signal Y is : " << _buttonThree._signalY << "\n"
            << "Signal Z is : " << _buttonThree._signalZ + _buttonThree._signalY << "\n";
        return;
    default:
        std::cout << "Button four output :\n"
            << "Signal X output is : " <<  _buttonFour._signalX << "\n"
            << "Signal Y output is : " <<  _buttonFour._signalY << "\n";
    }
}

Figure 3.

You should note that in the code in Figure 3 for two of the buttons (BUTTON_ID_2, BUTTON_ID_3) the signal Z is a sum of other signals. For the other two buttons this is not the case. This is another problem with the above implementation. We will see an alternative implementation later on. So lets try and tackle those problems. In order to improve the performance, we can start by removing all these switch and if statements. A common approach is to introduce a base abstract class declaring two functions, readButton and updateButtonLeds. Since we do not want to repeat ourselves, we could provide a template argument to that class, the button type, and have concrete classes implement the correct functionality.

Considering Figure 2, this approach would look like this:

template<class ButtonType>    
class Base
{
public:
    virtual ~Base(){}

    virtual void readButton() = 0;

    virtual void updateButtonLeds() = 0;
protected:
    ButtonType _button;
};

Figure 4.

So that would be our abstract class, where ButtonType is any of the structs presented in Figure 2. A concrete implementation might look like this:

class ConcreteOne : public Base<ButtonTypeOne>
{
    virtual void readButton();
    virtual void updateButtonLeds();
};

void ConcreteOne::readButton()
{
    _button._signalX = networkBus::getButtonOneSignalX();
    _button._signalY = networkBus::getButtonOneSignalY();
    _button._signalZ = networkBus::getButtonOneSignalZ();
}

void ConcreteOne::updateButtonLeds()
{
    std::cout << "Button one output :\n"
              << "Signal X output is : " << _button._signalX << "\n"
              << "Signal Y output is : " << _button._signalY << "\n"
              << "Signal Z output is : " << _button._signalZ << "\n";
}

Figure 5.

Using this approach we were able to remove all switch/if/else statements from our client code, which now looks like this:

void Classic_Polymorphism::Test()
{
    _buttonOne->readButton();
    _buttonTwo->readButton();
    _buttonThree->readButton();
    _buttonFour->readButton();

    _buttonOne->updateButtonLeds();
    _buttonTwo->updateButtonLeds();
    _buttonThree->updateButtonLeds();
    _buttonFour->updateButtonLeds();
}

Figure 6.

Unfortunately, the problem is now is that we need four concrete types for each of the four button types, because the button’s behaviors are similar but not identical. Additionally we introduced virtual tables and we still depend on explicit knowledge of the signal types. Adding virtual tables will certainly not help with our performance issues in comparison with the original C code. So we may want to reconsider our strategy here.

What if we could abstract our button objects in such a way that the client of these classes would have to know nothing about the signal types, thus removing the casts, while also providing type safety and flexibility for future implementations? How about if we were to also remove the virtual functions all together? Is this possible? By using traits it is. But what are traits?

In short, traits are important because they allow you to make compile-time decisions based on types, much as you would make runtime decisions based on values. Better still, by adding the proverbial “extra level of indirection” that solves many engineering problems, traits let you take the type decisions out of the immediate context where they are made. This makes the resulting code cleaner, more readable and easier to maintain. If you apply traits correctly, you get these advantages without paying the cost in performance, safety, or coupling that other solutions may exact.

So let’s start our next attempt to tackle this problem. We will begin by encapsulating all the networkBus functions mentioned before into function objects, also known as functors.

template< class Signal, Signal(*FUNC)()>
struct SignalWrapper
{
    typedef Signal SignalType;
    inline Signal operator()()const
    {
        return FUNC();
    }
};

Figure 7.

//typedefs for all available functions
typedef SignalWrapper<unsigned char, &networkBus::getButtonOneSignalX> ButtonOneSignalX;
typedef SignalWrapper<unsigned short, &networkBus::getButtonOneSignalY> ButtonOneSignalY;
typedef SignalWrapper<unsigned char, &networkBus::getButtonOneSignalZ> ButtonOneSignalZ;
typedef SignalWrapper<unsigned char, &networkBus::getButtonTwoSignalX> ButtonTwoSignalX;
typedef SignalWrapper<unsigned short, &networkBus::getButtonTwoSignalY> ButtonTwoSignalY;
typedef SignalWrapper<unsigned char, &networkBus::getButtonTwoSignalZ> ButtonTwoSignalZ;
typedef SignalWrapper<unsigned char, &networkBus::getButtonThreeSignalX> ButtonThreeSignalX;
typedef SignalWrapper<unsigned char, &networkBus::getButtonThreeSignalY> ButtonThreeSignalY;
typedef SignalWrapper<bool, &networkBus::getButtonThreeSignalZ> ButtonThreeSignalZ;
typedef SignalWrapper<unsigned short, &networkBus::getButtonFourSignalX> ButtonFourSignalX;
typedef SignalWrapper<signed char, &networkBus::getButtonFourSignalY> ButtonFourSignalY;

Figure 8.

While the previous step is purely cosmetic, it really reduces repetition and thus errors later on in the code. Our next step is to provide trait classes for the actual switches. If you remember Figure 2. there were three kind of buttons. Or were they only two? If we look at these classes again and abstract the different signal types, we realize that we only need two type of switch trait classes. One with two signals and one with three. Moreover we can have a base trait class which provides all the information we need for two signals and an extension of it for the additional third signal. So we come up with the following set of traits:

template<class SignalOneReader, class SignalTwoReader, int ID>
struct CommonButtonTraits
{
    typedef SignalOneReader ReadSignalOne;
    typedef SignalTwoReader ReadSignalTwo;

    typedef typename SignalOneReader::SignalType SignalOneType;
    typedef typename SignalTwoReader::SignalType SignalTwoType;
    static const int BUTTON_ID = ID;
}; 

template <class SignalOneReader, class SignalTwoReader, class SignalThreeReader, int ID>
struct ExtendedButtonTraits : public CommonButtonTraits<SignalOneReader, SignalTwoReader, ID>
{
    typedef SignalThreeReader ReadSignalThree;
    typedef typename SignalThreeReader::SignalType SignalThreeType;
};
typedef ExtendedButtonTraits<ButtonOneSignalX, ButtonOneSignalY, ButtonOneSignalZ,1> ButtonOneTraits;
typedef ExtendedButtonTraits<ButtonTwoSignalX, ButtonTwoSignalY, ButtonTwoSignalZ,2> ButtonTwoTraits;
typedef ExtendedButtonTraits<ButtonThreeSignalX, ButtonThreeSignalY, ButtonThreeSignalZ,3> ButtonThreeTraits;
typedef CommonButtonTraits<ButtonFourSignalX, ButtonFourSignalY,4> ButtonFourTraits;

Figure 9.

Note that we also provided type definitions for all the different buttons. We have now managed to encapsulate all information about signals into traits classes. Classes which use these traits do not need to know about the specifics of a signal type, or which function to call to actually read the signal. In addition, should a signal type change, the clients of the traits classes do not need to change a bit since they don’t contain any hardcoded information about the signal itself, rather they deduce the type from the template parameters themselves. Now we are ready for our final step. The button classes. Since we realized that we have two types of traits, we can also apply this to our actual button classes. So we can implement a base button class which uses two signals and an extended button class which is a base button class with an additional signal. We came up with this for the base class:

template<class ButtonTraits>
class ButtonBase
{
public:
    void read()
    {
        _signalX = typename ButtonTraits::ReadSignalOne()();
        _signalY = typename ButtonTraits::ReadSignalTwo()();
    }

    void update()
    {
        std::cout << "Button " << ButtonTraits::BUTTON_ID << "\n"
            << "Signal X is : " << _signalX << "\n"
            << "Signal Y is : " << _signalY << "\n";
    }
protected:
    typename ButtonTraits::SignalOneType _signalX;
    typename ButtonTraits::SignalTwoType _signalY;
};

Figure 10.

Pretty straightforward. And now on to the extended class:

template<class ButtonTraits, class EnableSignalAddition = void >
class ExtendedButton : public ButtonBase<ButtonTraits>
{
public:
    void read()
    {
        ButtonBase<ButtonTraits>::read();
        _signal = typename ButtonTraits::ReadSignalThree()();
    }

    void update()
    {
        ButtonBase<ButtonTraits>::update();
        std::cout << "Signal Z output is : " << _signal << "\n";
    }
private:
    typename ButtonTraits::SignalThreeType _signal;
};

Figure 11.

As you may recall, our output function for signal Z was different for buttons with id 2 and 3. We will tackle this problem in a future blog entry.

Following key points are to be noted:

  • No virtual methods.
  • No explicit hardcoded types about the signals. Instead the type is deduced from the respective traits.
  • Update functions are specialized for every type needed, to avoid code generation completely where it is not needed.

Imagine that a random signal X changes from bool to unsigned char. The classes in Figure 11 do not have to be modified at all. Only the typedef in Figure 8 would have to change and that’s it! We are ready to go! In the original implementation one would have to change the code in the client classes. We also saved CPU runtime due to the removal of

  • if/else/switch statements
  • Using vtables

and finally think about how flexible and at the same time type safe this design is in comparison with the original approaches. In the next blog we ‘ll talk about the enable_if idiom and code generation. So stay tuned.

Stefanos