Singleton
On Particle devices, using the singleton pattern is often handy. A singleton class only has a single instance per application, and is protected against creating more. This is common when interacting with a hardware peripheral where there can only be one of them, or other code where you only want one class instance.
Why a singleton?
Say, for example, you're communicating with a LCD display for your device. If you have a class to manage the display state and communicate with it, you probably only want one instance of the class. Not only will multiple instances take up valuable RAM, they could cause concurrent access to corrupt the display.
But why not a global?
A common design in Arduino is to create a global variable for the class in your main application file. This works, however there are some caveats:
The main issue is with global object constructors. There are numerous limitations on what you can include in a global object constructor because the order of initialization is not guaranteed by the C++ compiler. It's completely non-deterministic, so making even minor changes will sometimes cause the order to change unexpectedly. This can also be an issues on compiler upgrades.
Additionally, once more than one module needs the object, things start to get tricky. You need to pass an object reference to each module that needs it, which can run into a circular dependency problem. Accessing the global with extern
can run into the global object constructor ordering issue.
What is the solution?
The solution is to have each class maintain its own object pointer, which is allocated on the heap when first used. An object method returns this singleton instance.
Since global object construction is not used, all of the caveats pertaining to that are no longer an issue.
And since the object is not globally allocated, you can include the singleton in a library and if no code uses it, the linker will remove the code from your application, saving space. This is handy if you have code that's different on Wi-Fi and cellular, but you build on both. Or have code that's specific to Gen 2 or Gen 3 devices and build for both.
How do I do it?
This is not the only way you can implement the singleton pattern, however it's similar to how it's used in the Tracker Edge reference firmware, and it's one I've used in numerous libraries and is known to work well. The code walk-through corresponds to the code generated by the code generator, below.
Main application file
Your main application file will probably look something like this, but with more code.
#include "MyClass.h"
SYSTEM_THREAD(ENABLED);
SerialLogHandler logHandler;
void setup() {
MyClass::instance().setup();
}
void loop() {
MyClass::instance().loop();
}
- Include your header file, in this case,
#include "MyClass.h"
. - From
setup()
callMyClass::instance().setup()
. - From
loop()
callMyClass::instance().loop()
.
Not all applications will need both setup and loop, however you never really know when you might need it in the future and having both ready in every app will make things easier later if you need them. The overhead is very small.
MyClass::instance()
This construct is used for the singleton instance.
It's declared like this in the .h file:
static MyClass &instance();
and implemented like this in the .cpp file:
MyClass &MyClass::instance() {
if (!_instance) {
_instance = new MyClass();
}
return *_instance;
}
If the static class member
_instance
is NULL, then one is allocated withnew
. If it already exists, then the existing instance is returned quickly._instance
is a pointer (MyClass *
) but theinstance()
method returns a reference (MyClass &
).Both reference the existing object (not making a copy), but using a reference (
MyClass &
) allows the caller to useMyClass::instance().setup()
instead ofMyClass::instance()->setup()
. Both work, and it somewhat of a stylistic choice to which you prefer.
Some may notice that _instance is dereferenced without checking for NULL. This is intentional because if that allocation fails, you have a really serious out of memory condition, and propagating the error up greatly complicates all of the code because Particle devices do not have exceptions enabled. The code will cause a SOS+1 hard fault on out-of-memory, but this will typically occur during setup()
where if you will likely discover this during development and it will be obvious that something is very wrong.
Also, technically the implementation is not thread-safe. However, the way you use it is to instantiate it from early in setup(), before other threads are created, so in practice it will work properly if you use it in the way that is recommended.
Protected constructors
Just to make sure you don't accidentally try to instantiate MyClass
as a global, on the stack, or with new. The constructor is declared protected
. (private
would have worked a well). This will cause a compilation error if you use the singleton incorrectly.
protected:
/**
* @brief The constructor is protected because the class is a singleton
*
* Use MyClass::instance() to instantiate the singleton.
*/
MyClass();
/**
* @brief The destructor is protected because the class is a singleton and cannot be deleted
*/
virtual ~MyClass();
For example:
main.cpp:6:9: error: 'MyClass::MyClass()' is protected within this context
6 | MyClass myClass;
| ^~~
In file included from main.cpp:1:
MyClass.h:45:2: note: declared protected here
45 | MyClass();
| ^~~~~~~
main.cpp: In function 'void __static_initialization_and_destruction_0(int, int)':
Copy prevention
Another common programming error is trying to copy a pointer to the class to a variable or copying by reference. You shouldn't do that and should always call MyClass::instance()
and that's prevented with these two lines:
/**
* This class is a singleton and cannot be copied
*/
MyClass(const MyClass&) = delete;
/**
* This class is a singleton and cannot be copied
*/
MyClass& operator=(const MyClass&) = delete;
Thread support
Another common scenario is when you want to also implement a worker thread for your code, one per singleton class.
setup() (with thread)
While the plain boilerplate MyClass::setup()
doesn't do anything, the thread version does. It initializes the mutex to protect shared resources and creates the thread.
The constant 3072
is the size of the stack (3K). You can adjust this up or down as desired.
void MyClass::setup() {
os_mutex_create(&mutex);
thread = new Thread("MyClass", [this]() { return threadFunction(); }, OS_THREAD_PRIORITY_DEFAULT, 3072);
}
Thread function as a class member
This code is a handy trick:
[this]() { return threadFunction(); }
The declaration of the thread function is std::function<os_thread_return_t(void)>
. It's a function with this declaration:
os_thread_return_t myFunction(void);
It cannot be a C++ member function that's not declared static
because the this
pointer is not available to the callback.
Since the parameter is a std::function
, however, it can be a C++11 lambda, which is what the expression [this]() { return threadFunction(); }
is.
It captures the this
pointer, then uses it to call the threadFunction() which is a C++ member function.
Another way to solve this problem is to create a static member function like this and pass threadFunctionStatic
when creating the thread.
static void threadFunctionStatic(void) {
MyClass::instance().threadFunction();
}
Mutex wrappers
The code generator also creates lock methods:
/**
* @brief Locks the mutex that protects shared resources
*
* This is compatible with `WITH_LOCK(*this)`.
*
* The mutex is not recursive so do not lock it within a locked section.
*/
void lock() { os_mutex_lock(mutex); };
/**
* @brief Attempts to lock the mutex that protects shared resources
*
* @return true if the mutex was locked or false if it was busy already.
*/
bool tryLock() { return os_mutex_trylock(mutex); };
/**
* @brief Unlocks the mutex that protects shared resources
*/
void unlock() { os_mutex_unlock(mutex); };
These are compatible with WITH_LOCK()
.
For example, you might have code like this to make sure only one thread can access your peripheral device at a time.
uint8_t MyClass::readStatusRegister() {
WITH_LOCK(*this) {
return readRegister(REG_STATUS);
}
}
thread function
Finally, you put the code you want to run in the threadFunction
. It's a class member so it has access to all class members (including the mutex). It typically runs forever - you never return from it.
In order to yield CPU time to other threads, you should call delay(1)
to allow the next thread to execute.
os_thread_return_t MyClass::threadFunction(void) {
while(true) {
// Put your code to run in the worker thread here
delay(1);
}
}
Code generator
It's not difficult to write the necessary boilerplate code, but it is an annoying repetitive task. You can use the tool below to generate the singleton and optional thread code to use as the basis of your new class.
Class Name | ||