Class or Module for Singletons?

published: July 9, 2019 — last modified: October 23, 2025

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 (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. 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 , 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 . Only because you are using an object-oriented language like C++ to write a firmware, does not mean you you have to use a class as a module in any case. 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.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. 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.

 

 

 

 

More Posts

Snowflake Configuration

Posted on 2019-11-22— Projects

I walk you through the Snowflake v1.2 firmware's button-based configuration: short vs long presses, auto and single modes, LED indicators, and pattern durations. I explain the six-second timeout and that settings aren't saved after power loss—please read the full post for step-by-step usage and tips.

Read this post

Boards in Good Quality from SeeedStudio Fusion

Posted on 2018-02-11— Review

I tested SeeedStudio's Fusion PCB service for a new prototype and ordered five blue, 2-layer FR‑4 boards with lead‑free HASL. Delivery took about 10 days and the boards showed good quality for the price. Read on for details about ordering, solder mask, and tolerances.

Read this post

Perfect Prototype Boards from Eurocircuits

Posted on 2017-10-02— Projects

I received prototype boards from Eurocircuits and was impressed by their outstanding build quality—sharp solder-mask edges, precise alignment, and a subtle shine on the traces. I share close-up photos and brief thoughts about vias and finishes; read on if you appreciate careful PCB craftsmanship.

Read this post

Adafruit PowerBoost 1000C Enclosure (LR2165)

Posted on 2021-08-21— 3D Printing, Projects

I designed a simple three-part 3D printed enclosure for the Adafruit PowerBoost 1000C and up to a 1200mAh LiPo. In this post I share the print files, component list, and step-by-step assembly tips to convert the board into a safe portable power pack — please read on if you'd like to build one.

Read this post

The 3D Printed Modular Lantern

Posted on 2023-01-17— 3D Printing, Projects

I designed a modular 19th‑century‑style lantern you can 3D print as a candlelight or expand into an LED street‑lamp stand. Read on for my glue‑free assembly, colour‑layer printing tips, and stand options — or jump to the Printables page for downloads and print instructions.

Read this post

Event-Based Firmware (Part 2/2)

Posted on 2019-07-08— C++, Improve your Code, Learn

I guide you through evolving a simple Arduino blink example into a reusable event system. In Part 2 I cover function-pointer events, an EventLoop module, and practical multi-event examples. If you want cleaner, non-blocking firmware structure, read the full post for code and explanations.

Read this post