Class or Module for Singletons?

Should you use a class or a module with a namespace for a singleton interface in your firmware? I found there are many misunderstandings which lead beginners to make a wrong decision in this matter. With this article, I try to visualize these misunderstandings with simple example code for the Arduino platform.

Before we start, as with all of these topics, there is no simple rule, and there are a lot of exceptions. In the end, it heavily depends on the compiler and architecture you use.

The Example Use Case

I like to write simple driver code for my firmware, which flashes two LEDs for a given duration. The used PINs for the LEDs shall be configurable. In my main loop, I will flash the two LEDs at different durations.

The use case is no real-world example, but it contains all elements of configuration, initialization and usage.

Using a Simple Class

For the first test case, I write a simple class, without constructor and all required methods for the use case.

LedDriver.hpp

#pragma once

#include <Arduino.h>

class LedDriver
{
public:
  void setOrangePin(uint8_t orangePin);
  void setGreenPin(uint8_t greenPin);
  void initialize();
  void flashOrange(uint32_t duration);
  void flashGreen(uint32_t duration);

private:
  uint8_t _orangePin;
  uint8_t _greenPin;
};

LedDriver.cpp

#include "LedDriver.hpp"

void LedDriver::setOrangePin(uint8_t orangePin)
{
  _orangePin = orangePin;
}

void LedDriver::setGreenPin(uint8_t greenPin)
{
  _greenPin = greenPin;
}

void LedDriver::initialize()
{
  pinMode(_orangePin, OUTPUT);
  pinMode(_greenPin, OUTPUT);
}

void LedDriver::flashOrange(uint32_t duration)
{
  digitalWrite(_orangePin, HIGH);
  delay(duration);
  digitalWrite(_orangePin, LOW);
  delay(100);
}

void LedDriver::flashGreen(uint32_t duration)
{
  digitalWrite(_greenPin, HIGH);
  delay(duration);
  digitalWrite(_greenPin, LOW);
  delay(100);  
}

class_or_module1.ino

#include "LedDriver.hpp"

LedDriver ledDriver;

uint32_t gDuration = 100;

void setup() {
  ledDriver.setOrangePin(13);
  ledDriver.setGreenPin(12);
  ledDriver.initialize();
}

void loop() {
  ledDriver.flashOrange(gDuration);
  ledDriver.flashGreen(gDuration);
  gDuration += 20;
  if (gDuration > 500) {
    gDuration = 100;
  }
}

The usage of this driver is straightforward. I create a new instance of this class LedDriver ledDriver; and call the methods on it to configure, initialize and use it.

Compiling this code for an Arduino Uno will get these results:

Sketch uses 1146 bytes (3%) of program storage space. Maximum is 32256 bytes.
Global variables use 15 bytes (0%) of dynamic memory, leaving 2033 bytes for local variables. Maximum is 2048 bytes.

Using a Module with a Namespace

As a second example, I write the same interface using a module with a namespace and the same functions. Also I move the instance variables as global variables into the implementation file.

LedDriver.hpp

pragma once

#include <Arduino.h>

namespace LedDriver {

void setOrangePin(uint8_t orangePin);
void setGreenPin(uint8_t greenPin);
void initialize();
void flashOrange(uint32_t duration);
void flashGreen(uint32_t duration);

}

LedDriver.cpp

#include "LedDriver.hpp"

namespace LedDriver {

uint8_t _orangePin;
uint8_t _greenPin;

void setOrangePin(uint8_t orangePin)
{
  _orangePin = orangePin;
}

void setGreenPin(uint8_t greenPin)
{
  _greenPin = greenPin;
}

void initialize()
{
  pinMode(_orangePin, OUTPUT);
  pinMode(_greenPin, OUTPUT);
}

void flashOrange(uint32_t duration)
{
  digitalWrite(_orangePin, HIGH);
  delay(duration);
  digitalWrite(_orangePin, LOW);
  delay(100);
}

void flashGreen(uint32_t duration)
{
  digitalWrite(_greenPin, HIGH);
  delay(duration);
  digitalWrite(_greenPin, LOW);
  delay(100);  
}

}

I kept the variable names _orangePin and _greenPin for a better comparison. Usually, I would have named them gOrangePin and gGreenPin.

class_or_module2.ino

#include "LedDriver.hpp"

uint32_t gDuration = 100;

void setup() {
  LedDriver::setOrangePin(13);
  LedDriver::setGreenPin(12);
  LedDriver::initialize();
}

void loop() {
  LedDriver::flashOrange(gDuration);
  LedDriver::flashGreen(gDuration);
  gDuration += 20;
  if (gDuration > 500) {
    gDuration = 100;
  }
}

There is no instance required to use this interface. Beside this small detail, there is no considerable difference.

Compiling this code for an Arduino Uno will get these results:

Sketch uses 1144 bytes (3%) of program storage space. Maximum is 32256 bytes.
Global variables use 15 bytes (0%) of dynamic memory, leaving 2033 bytes for local variables. Maximum is 2048 bytes.

It is an almost similar result. The firmware is two bytes smaller. These bytes are missing, because the global variables are not initialized.

Comparing the Generated Code

If we compare the generated code for the two examples, there is almost no difference. The optimizer of the compiler reduced everything to very similar code, as you can see in the disassembly below:

The generated code for the first example is on the left side and the code for the second example on the right side.

Most notable is the way, how the pin configuration is accessed: For the class-based interface, it looks like this:

lds	r24, 0x0105	; 0x800105 <__data_end+0x1>

The second implementation generated code, as shown below:

mov	r24, r17

So, there is no Difference?

If you look at these examples, you may think it does not matter if you implement a singleton as a class or as a collection of functions in a namespace. In this perticular case, the optimizer did its best and reduced everything to almost identical machine code.

Let us compare the two implementations for a singleton interface for an embedded platform:

The Class

  • Pro: The instance variable is a user-defined name to access the interface.
  • Pro: Classes provide a higher level of abstraction.
  • Pro: There are no “hidden” global variables.
  • Con: It is not clear the class represents a singleton. A developer can create multiple instances accidentally.
  • Con: To access the interface, you need to add additional code to pass the instance variable to other modules.
  • Con: It usually produces larger firmware (explained later).

The Namespace

  • Pro: Is a singleton by definition and exists only once.
  • Pro: Can be used anywhere just by including the header file.
  • Pro: It usually produces the smallest firmware size.
  • Con: The global variables are “hidden” in the implementation.
  • Con: There is only one layer of abstraction.

Adding a Constructor to the Class

You may not have implemented the class as shown in the first example. Usually, a driver like this comes with a constructor where you can configure the instance.

LedDriver.hpp

#pragma once

#include <Arduino.h>

class LedDriver
{
public:
  LedDriver(uint8_t orangePin, uint8_t greenPin);
  void initialize();
  void flashOrange(uint32_t duration);
  void flashGreen(uint32_t duration);

private:
  uint8_t _orangePin;
  uint8_t _greenPin;
};

LedDriver.cpp

#include "LedDriver.hpp"

LedDriver::LedDriver(uint8_t orangePin, uint8_t greenPin)
  : _orangePin(orangePin), _greenPin(greenPin)
{    
}

void LedDriver::initialize()
{
  pinMode(_orangePin, OUTPUT);
  pinMode(_greenPin, OUTPUT);
}

void LedDriver::flashOrange(uint32_t duration)
{
  digitalWrite(_orangePin, HIGH);
  delay(duration);
  digitalWrite(_orangePin, LOW);
  delay(100);
}

void LedDriver::flashGreen(uint32_t duration)
{
  digitalWrite(_greenPin, HIGH);
  delay(duration);
  digitalWrite(_greenPin, LOW);
  delay(100);  
}

class_or_module3.ino

#include "LedDriver.hpp"

LedDriver ledDriver(13, 12);

uint32_t gDuration = 100;

void setup() {
  ledDriver.initialize();
}

void loop() {
  ledDriver.flashOrange(gDuration);
  ledDriver.flashGreen(gDuration);
  gDuration += 20;
  if (gDuration > 500) {
    gDuration = 100;
  }
}

This implementation looks smaller, with fewer functions to call. If we compile this example, we get the following results:

Sketch uses 1186 bytes (3%) of program storage space. Maximum is 32256 bytes.
Global variables use 15 bytes (0%) of dynamic memory, leaving 2033 bytes for local variables. Maximum is 2048 bytes.

Even this code seems smaller; it generates 44 bytes larger firmware. The reason is the constructor of the class. As soon as you introduce a custom constructor, the compiler will generate additional instructions to construct an object and also creates a function table.

The additional code can not easily be optimized away. Therefore the firmware size grows, without any additional benefits.

Size Comparison

NamespaceClass, no ctorClass + ctor
Code Size114411461186
Used Ram151515

Runtime Behaviour

Until now, I just discussed the impact of the different implementations on the size of the firmware. Another topic is the runtime behaviour of the function calls.

All three implementations will have the same runtime behaviour, as long as there is only one instance of the class in the firmware. The optimizer will detect this case and precalculate all variable references, like this->_greenPin, using absolute addresses.

As soon as you introduce multiple instances of a class, there may be an additional cost for each call. In this case, the instance location is put to the stack* prior the actual call, which takes extra time. Also, the indirect memory access is slower in this case.

*=Most likely put into a register for optimized code.

Bad Code (Code with Potential)

There are some situations where you should rethink your current implementation and consider an alternative.

Class with no Instance Variables

class Foo {
public:
  Foo();
  void methodOne();
  void methodTwo();
};

If you see a regular class with no instance variables, you should analyze why this construct exists as class at all. Each instance of this class will be equal unless you are using the pointer to class instances as data.

It is usually some driver interface, and all method implementations access hardware registers or other global variables.

Something like this would make sense in a class hierarchy, as an interface or abstract base class, but not on its own.

Class with only Static Elements

class Foo {
public:
  static void methodA();
  static void methodB();
private:
  static uint32_t _value;
};

In this case, the class is used as a namespace. It makes no sense to create an instance of a class like this. You should convert this into a module using a namespace like this:

namespace Foo {
void methodA();
void methodB();
}

The private variable _value is moved to the implementation file as a global variable in the Foo namespace.

Ancient C Interface

struct Data {
  uint8_t a;
  uint8_t b;
}

void dataInitialize(Data *data);
void dataFunctionA(Data *data, uint32_t value);
void dataFunctionB(Data *data, uint8_t x);

The code above could be from a C developer, which did never made the transition to object-oriented languages. You should rewrite this code into a class like this:

class Data {
public:
  void Data();
  void methodA(uint32_t value);
  void methodB(uint8_t x);
private:
  uint8_t _a;
  uint8_t _b;
};

Source Code of All Examples

You can find all the examples from this article in the following GitHub repository:

https://github.com/LuckyResistor/class-or-module

Learn More

Conclusion

Here my recommendation for embedded software:

  • If you are working with really tight size constraints (e.g. ATtiny22), you should favour the procedural approach using namespaces and modules. It provides good isolation of your implementation and one level of abstraction, which is enough for most cases in embedded code. The optimizer of the compiler will see less complexity and will most likely produce the smallest machine code possible.
  • If you have no or little size constraints, you should consider the advantages and disadvantages of both solutions. Only because you are using an object-oriented language like C++ to write a firmware, does not mean you have to use a class as a module in any case.
    Especially embedded software uses many interfaces where only one single instance can exist – because it is bound to a specific hardware peripheral. Using a couple of functions in a namespace will reduce the complexity of the code and provides enough abstraction for most cases. Also, there is no considerable difference if the global variables are defined in the implementation itself, or somewhere else as the single instance of the interface.
  • If you are writing desktop software, where you have no size and speed constraints, you should prefer a class before a flat procedural interface. In this case, you should also follow common patterns for singletons and implement guards to protect the developer from creating multiple instances. Also, you should implement an interface to make this singleton accessible from the right places in your code.

If you have questions, miss some information or have any feedback, feel free to add a comment below.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.