Creating a library: Using classes or not?

One other option for an OOP solution even when there will only be one object instantiated is to force that object to be a Singleton. This gives you the nice encapsulation and abstraction of a class and ensures that the user won’t be mucking about in your namespaces. The example below is a variant on the technique @PieterP posted here. The library provides a single instance of the class. Because the constructor is private, and the copy-constructor and operator= are deleted, it’s impossible for the user to instantiate any others.

MySingletonClass.h:

#ifndef MYSINGLETONCLASS_H_
#define MYSINGLETONCLASS_H_

#include "Arduino.h"

class MySingletonClass {
public:
	MySingletonClass(const MySingletonClass &) = delete; // no copying allowed
	MySingletonClass &operator=(const MySingletonClass &) = delete; // no assignment allowed
	static MySingletonClass &getInstance(); // Accessor for singleton instance

	void examplePublicInstanceFunction();
	static void examplePublicClassFunction();

	uint8_t examplePublicInstanceVariable {0};
	static uint8_t examplePublicClassVariable;

private:
	MySingletonClass();  // Constructor is private
	void examplePrivateInstanceFunction();
	static void examplePrivateClassFunction();

	uint8_t examplePrivateInstanceVariable {0};
	static uint8_t examplePrivateClassVariable;
};

extern MySingletonClass &singleton;

#endif /* MYSINGLETONCLASS_H_ */

MySingletonClass.cpp:

#include "MySingletonClass.h"

uint8_t MySingletonClass::examplePrivateClassVariable { 0 };
uint8_t MySingletonClass::examplePublicClassVariable { 0 };

MySingletonClass::MySingletonClass() {  // Private constructor
}

MySingletonClass & MySingletonClass::getInstance() {
  static MySingletonClass instance;
  return instance;
}

void MySingletonClass::examplePublicInstanceFunction() {
}

void MySingletonClass::examplePublicClassFunction() {
}

void MySingletonClass::examplePrivateInstanceFunction() {
}

void MySingletonClass::examplePrivateClassFunction() {
}

MySingletonClass &singleton {MySingletonClass::getInstance()};

The main .ino file uses the one (and only possible) instance of the class named “singleton”:

#include "Arduino.h"

#include "MySingletonClass.h"

void setup() {
	singleton.examplePublicClassVariable = 100;
	singleton.examplePublicInstanceVariable = 200;

	singleton.examplePublicInstanceFunction();
	singleton.examplePublicClassFunction();
}

void loop() {
}

One of the main advantages of encapsulation using classes (singletons or otherwise) is discoverability: When you type class_instance. a decent IDE with autocomplete (including the Arduino IDE 2.0) will give an overview of the different methods and accessible members of the class. This makes it very easy for new users to discover what functionality is available without having to switch to the documentation every 10 characters.

Free functions are harder to use with autocomplete because it doesn’t narrow down the list of suggestions for a given context. There are languages with uniform function call syntax, which solves exactly this dilemma between writing classes or free functions. There have been multiple proposals to add it to C++, but none of them got standardized yet.

I’d like to point out that choosing between classes and free functions does not have to be mutually exclusive: it often makes sense to implement certain logic and algorithms as free functions, and then glue them together into a coherent interface using a class.
For example:

namespace detail {
int *binary_search(int *begin, int *end, int target) {
  ...
}
} // namespace detail

class SomeContainer {
  private:
    int storage[100];
    size_t size = 0;
  public:
    void insert(int) { ... }
    void remove(int) { ... }
    int *find(int target) { 
        return detail::binary_search(storage, storage + size, target);
    }
};

Using free functions like this can greatly improve code reuse and testability.
Note that I used a namespace to avoid polluting the global namespace with dozens or hundreds of free functions, as to not contribute to the autocomplete problem mentioned above.

If this meant to be an educational library, it might be a good idea to gradually expose the students to all three concepts (free functions, namespaces and classes).

Singletons are certainly useful in some scenarios, but you have to be careful not to overuse them. For example, someone writing code for an Arduino Uno might be tempted to make the class for the Serial port a singleton, because the Uno only has one. But then he/she might switch to an Arduino Mega, which has multiple serial ports, and it turns out that a singleton wasn’t the best idea, and a “normal” non-copyable class with predefined global instances would have been a better design choice. (Which is exactly what the Arduino developers opted for.)

As a final note, I believe it’s good practice to wrap your entire library in a namespace. It prevents name collisions, both at compile time and at link time. There are plenty of libraries that define a global enum { Off, On }, for example. Generic names like that are bound to collide with other libraries.
If you want, you can hide the namespaces from your users as follows:

// MyClass.hpp
namespace mylib {
class MyClass { ... };
} // namespace mylib
// MyLibrary.hpp (main header that users will include in their Arduino sketch)
#include "MyClass.hpp"

#ifndef MY_LIBRARY_NO_USING_NAMESPACE
using namespace mylib;
#endif

This makes the namespace invisible to beginners who might not expect a namespace in an Arduino library, but it does prevent name collisions with other libraries. If there are any collisions or ambiguities, these can easily be resolved by specifying the namespace explicitly, or by defining the MY_LIBRARY_NO_USING_NAMESPACE macro before including your library.
Without namespaces, there is no way to resolve the ambiguities except modifying the library source files or rewriting your code to never use the two libraries in the same sketch/file (which is often not an option, and even then you might end up with problems at link time).

1 Like

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.