C++ Classes
This chapter is not intended do much more than give you a taste of C++. We will explore enough (I hope) of this enhanced language to make use of C++ classes to complement our C programming skills and, in turn, enhance our Arduino programs.
As you know, in C, the ++ operator can be used to increment a value by 1. So, the programming language called C++, designed by Bjarne Stroustrup, was intended as an incremental advance upon C. Principally the new language introduced the concept of classes although there were a raft of other features collectively intended to meet the developing needs of programmers tackling larger and more complex projects. Because C++ is backwards compatible with C, the Arduino IDE can make use of an up-to-date, optimizing C++ compiler to generate machine code from our programs and we are free to make use of any C++ features that catch our fancy. These have already included function and operator overloading so now we can turn our attention to classes.
The vast majority of libraries shared within the Arduino community are constructed using one or more C++ class. Classes are the cornerstone of object-oriented programming (OOP) as they support encapsulation and inheritance. Most general introductions to objects and classes pay a lot of attention to inheritance but, from our perspective, encapsulation is of primary interest. Now is the moment to introduce both terms properly.
Encapsulation
Encapsulation is the OOP concept that binds together the data and methods (functions) needed to manipulate the data for a given object. Classes can be anything from software representations of real world objects (like houses, cars or cameras) to more abstract objects such as images, musical tracks or financial transactions. A class can control access to an object’s data by presenting methods to read, set or manipulate the data. This prevents external code from making changes to the state of a given object but also, of course, relieves the main code body from having to be concerned with how something happens when all it needs to do is make an appropriate request to a given class. Classes are used to generate instances of an object and each instance can have a different state while sharing a common code base. If I have a class instance for each wheel on a robot then they can be manipulated (rotating in different directions or at different speeds) independently using a common interface (a set of commands and requests for data).
Inheritance
Classes can be arranged in a hierarchy that represents some classes as “is a type of” some parent class. While complex class hierarchies can quickly become unwieldy (if not unmaintainable) they can in the right circumstances be very effective. Consider the visual objects presented by a Graphical User Interface (GUI) such as the user presentation layer from Windows or the Apple Mac operating systems. Here, the more complex objects (perhaps a drop-down list) are derived from simpler screen objects like labels. Programmers can join in the game and subclass a screen widget to add additional features or to override some specific functionality (method) with something new. The opportunities to use inheritance to create families of objects are going to be somewhat limited within the software confines of an Arduino although the related (no pun intended) concept of composition, where classes contain instances of other classes might be an effective strategy for dealing with complex control or monitoring projects.
The elements of a C++ class
A C++ class is a user defined type (just as a struct is a user defined type) that contains functions (called methods) as well as data elements. Methods and data items have an accessibility setting of public, protected or private. Private items are not directly accessible from code external to the class and this is the default status for a class member.
For our purposes, a C++ class is normally implemented using two files. One is a header file containing the names, types and accessibility of each class member. The header file is used to include and make available a class to an Arduino program (sketch). The second C++ file contains code for each of the C++ methods.
To explore this common structure, we could outline a class to support one or more Arduino controlled door lock. This will help get the feel for how a class is defined. Our notional lock might have a release button to be used by someone the right side of the lock and some mechanism to send an entry code from the wrong or outside of the locked door. We would also need a method to set the door code and we should make some provision for some set-up data as well as provide a method to unlatch the lock.
Starting from scratch, create a new program in the Arduino IDE and save it as (say) DemoClass. Then click the little button with a down arrowhead on the tab bar and select the “New Tab” option from the context menu. You will be invited to supply a file name – enter Dlock.cpp. Then do the same again and create a file called Dlock.h. You should then see three tabs in the IDE, one for the original program file and the two we have just created. We should start with the header file – Dlock.h.
By convention the header file should start with two lines similar to these:
#ifndef Dlock_h
#define Dlock_h
With the header file content ending with
#endif
The reason for the #ifndef and #endif lines is that in some cases a given header file might be included into more than one element of a program and its libraries and local classes. These lines ensure that the header content is only evaluated if the header has not already been included by the compile process. While it might not be likely that a clash will occur in the case of a class you are defining there is no downside to including this check so go with the flow.
We can start the class definition by adding the following to the header file:
class Dlock
{
public:
private:
};
The class members (data declarations and function prototypes) are listed under the relevant labels as “public” or “private”. Strictly speaking, the “private” label is not necessary as any members listed before the public label will be treated as private. However, I am in favour of being explicit whenever possible as it avoids (well minimises) future errors. What happened to the “protected” label? Well, the protected classification is used for class members that are accessible from classes that extend (that are inherited from) the class but would be treated as private as far as code external to the class is concerned. We don’t need any protected class members for this exercise.
You might wonder why some class members should be private and thus inaccessible to external code. If you were writing a C++ class to be used in a large project with (perhaps) dozens of other programmers creating and manipulating instances of your class then you would want to ensure that the internal workings of your class could not be disrupted in error or overridden by over enthusiastic fellow team members. If this class is only going to be used by your own code then you might argue that all class members could be public and you could avoid having to decide on the classification on a case by case basis. I am going to suggest that you think about this in terms of the API (Application Programming Interface). If a method is not required to interface correctly with a class then it should be classed as private. If a data item needs to be set from external code then you could expose it as public but if you feel any values being set should be validated then perhaps the variable should be private with a public method to accept, check and set new values. Following this approach, the API is defined by the public members and any future user (even if that is you) can be confident that the other components of the class do not need to be addressed in order to exploit the full functionality of the class.
So let’s fill in some of the members in our notional lock class. A great number of classes need to set some attributes as soon as they are invoked. Most of those will probably need to make use of some parameters to ensure they are set up correctly for their particular purpose. Both tasks are managed by a method known as a constructor. A class constructor method takes the same name as the class. If a constructor is required then we need to add a prototype for it to the public group of class methods. Constructors do not have a return type.
Class Dlock
{
public:
Dlock(int, int);
private:
};
In the above example I have defined a constructor that takes 2 int parameters. Constructors can be overloaded – perhaps where a class may be invoked with different parameters leaving others to be set by other methods or left as default values.
A class may also need a destructor to be called when a class is no longer required but specific actions might need to be taken to release resources. A typical reason for needing a destructor would be that the class might have used malloc() to allocate some memory and thus a destructor would be required to ensure that the memory was freed. A destructor takes the same name as any constructor but with a tilde (~) as a name prefix. A destructor can be called explicitly or it may be called automatically (if available) when a class instance goes out of scope. If a class instance is created within a function (or other code block), then any available destructor will be called to clean up when the function (or code block) comes to an end.
I am going to fill in a few more class members so that we can start to outline some methods in the C++ code file.
class Dlock { public: Dlock(int, int); Dlock(); ~Dlock(); void SetPins(int, int); bool IsDoorOpen(); bool ReSetCode(char[]); bool ValidateCode(char[]); void OpenLock(); private: int lockOpenPin; int doorClosedPin; bool isLockOpen; bool saveNewCode(); };
If you have typed these lines into the header tab, click the Dlock.cpp tab and add #include statements for Arduino.h and Dlock.h. As Arduino.h is probably included into every Arduino library code file as well as automatically by the IDE compile and build process we can be pretty sure that it starts with a #ifndef line (just saying).
We can then start to fill in some of the methods, starting with the constructors.
#include <Arduino.h>
#include "Dlock.h"
Dlock::Dlock(int loPin, int dcPin) {
lockOpenPin = loPin;
doorClosedPin = dcPin;
}
Dlock::Dlock() {
// empty constructor
}
Dlock::~Dlock() {
// needs to be defined if it is included in the header file.
}
void Dlock::SetPins(int loPin, int dcPin) {
if(loPin > 1) {
lockOpenPin = loPin;
}
if(dcPin > 1 && dcPin != loPin) {
doorClosedPin = dcPin;
}
}
bool Dlock::IsDoorOpen() {
return isLockOpen;
}
In the example methods, you will see the class name followed by two colons before the method name. The two colons are the scope resolution operator. This new operator is used with the class name to ensure there is no ambiguity – the method is a member of this specific class. The code file could contain other C++ code not related to the specific class. While not always appropriate, you will see an example later in this book where code not strictly part of a class is added to a class C++ code file.
Before we go too far with a class we are not going to build into a working lock, we should take a look at how the class might be used within an Arduino C program.
#include "Dlock.h" Dlock frontDoor(2, 3); Dlock backDoor; void setup() { backDoor.SetPins(5, 6); } void loop() { }
The program code includes the class header file and then goes on to create two instances of the class. The first instance (frontDoor) uses the main constructor with two integer values. The second (backdoor) uses the empty constructor. Note that the declaration using a constructor that does not take any arguments should not include even empty function brackets. In this tiny sample, the setup() function includes a line that is a method call to the backDoor class instance. Note that, like addressing values within a struct, dot notation is used to identify the class method being addressed.
Now we have run through a quick introduction to C++ classes it might be time to tackle one we might well use. We could quickly work up a C++ class implementation of our generalised FiFo queue, previously written and maybe installed as a C library.
I have called the class in this version FiFoQC as I will want to install it alongside the “pure” C version as a library and will therefore need a different name.
As we know what functions we are going to need, we can kick off with the header file:
#ifndef FiFoQC_h #define FiFoQC_h class FiFoQC { public: FiFoQC(size_t dataSize); ~FiFoQC(); bool queueAdd(void* dta); void* readFirst(); void* readNext(); bool deleteFirst(); int getQCount(); void zapQueue(); private: struct FiFoNode { void *data; struct FiFoNode* next; }; struct FiFoQ { struct FiFoNode* head; struct FiFoNode* tail; struct FiFoNode* curPos; int count; size_t dataSize; }; struct FiFoQ* FiFoQP; }; #endif
You will see that the class has a constructor that takes a single argument – the size of the data items we intend to add to the queue. There is also a class destructor because of the use of malloc() to grab memory and we need to be sure it is all released when the class is no longer required. The next function queueAdd() no longer requires a pointer to the queue struct in memory and just has a pointer to the data to be stored as a parameter. You might notice a new function readNext() that was not part of the C version – this would simplify (say) data sampling should that be required in a future program.
The queue specific struct declarations are all tucked away in the “private” area as anyone using this class API has no need to think about them.
Now the C++ code file in chunks:
#include "Arduino.h" #include "FiFoQC.h" FiFoQC::FiFoQC(size_t dataSize) { FiFoQP = malloc(sizeof(struct FiFoQ)); FiFoQP->head = FiFoQP->tail = FiFoQP->curPos = NULL; FiFoQP->dataSize = dataSize; FiFoQP->count = 0; } FiFoQC::~FiFoQC() { this->zapQueue(); free(FiFoQP); }
bool FiFoQC::queueAdd(void* dta){ struct FiFoNode* node = malloc(sizeof(struct FiFoNode)); if(node == NULL) { return false; } node->next = NULL; void* dat = malloc(FiFoQP->dataSize); if(dat == NULL) { free(node); return false; } memcpy(dat, dta, FiFoQP->dataSize); node->data = dat; if(FiFoQP->head == NULL) { FiFoQP->head = FiFoQP->tail = node; } else { FiFoQP->tail->next = node; FiFoQP->tail = node; } FiFoQP->count++; return true; }
Here we see the first of the original queue functions being converted to a class method. The method name is prefixed by the class name and the scope resolution operator. Methods are clearly tagged as belonging to a specific class and this is important because a given code file might also contain general helper functions or code for another closely related class.
void* FiFoQC::readFirst() { struct FiFoNode* p = FiFoQP->head; if(p) { FiFoQP->curPos = p; return p->data; } else { return FiFoQP->curPos = NULL; } }
void* FiFoQC::readNext() { struct FiFoNode* p = FiFoQP->curPos->next; if(p) { FiFoQP->curPos = p; return p->data; } else { return FiFoQP->curPos = NULL; } }
bool FiFoQC::deleteFirst() { if(FiFoQP->head == NULL) { return false; } struct FiFoNode* node = FiFoQP->head; struct FiFoNode* nxt = node->next; free(node->data); free(node); FiFoQP->head = nxt; if(nxt == NULL) { FiFoQP->tail = NULL; } FiFoQP->count--; return true; } int FiFoQC::getQCount(){ return FiFoQP->count; } void FiFoQC::zapQueue() { bool res = this->deleteFirst(); while(res){ res = this->deleteFirst(); } }
Did you notice the other small change to the readFirst() function (now a method)? That supports the new facility to continue reading queue data items without deleting any entries. The function saves a pointer to the read item that can be updated by the readNext() method as items are read in turn. This adds a little more “overhead” to the queue performance together with a small increase in memory usage. The balance between additional functionality and performance is always going to be an issue when programming for Arduinos.
Now some test code for the main code tab to show how the class might be used, including the new readNext() method.
#include "FiFoQC.h"
template<class T> inline Print &operator <<(Print &obj, T arg) { obj.print(arg); return obj; }
struct MyData {
int myVal;
int yourVal;
};
FiFoQC fiFoQ(sizeof(MyData)); // Create a class instance
void setup() {
Serial.begin(115200);
MyData myData;
MyData* mDta;
for(int i = 1; i < 21; i++) {
myData.myVal = i << 2;
myData.yourVal = i;
fiFoQ.queueAdd(&myData); // Add data items to the queue
}
Serial << "Queue has " << fiFoQ.getQCount() << " items loaded\n";
Serial << "Now reading records in sequence\n";
mDta = fiFoQ.readFirst();
while(mDta) {
Serial << "Read myval: " << mDta->myVal << " yourVal: " <<
mDta->yourVal << "\n";
mDta = fiFoQ.readNext(); // read queue items in sequence
}
Serial << "Now reading and deleting records in sequence\n";
mDta = fiFoQ.readFirst();
while(mDta) {
Serial << "Read myval: " << mDta->myVal << " yourVal: " <<
mDta->yourVal << "\n";
fiFoQ.deleteFirst();
mDta = fiFoQ.readFirst();
}
Serial << "Queue now has " << fiFoQ.getQCount() << " items\n";
fiFoQ.~FiFoQC(); // call the destructor
}
Please give the program and new C++ class a test run.
This C++ class could now be installed as a library available to the Arduino IDE.
Having established that C++ classes are a great way to create a library we had better take a deeper look at classes, their data and methods (collectively called members) and then pointers to members.
Static Class Members
Class members (data and methods) can be declared as static and this has a different meaning to the static modifier used for variables declared in a function. Static members do not need to be associated with a class instance. Irrespective of the number of class instances created, static members are shared by all instances and can also be addressed directly without creating a class instance. A static method can be used without creating a class instance just like a static class variable member. A static method can only access other static class members of a class. So, what are they for?
Methods may be declared as static when they provide a general “service” associated with a specific class type but where there is no need to reference (or create) a class instance for them to function. Usually we would be talking about “helper” methods that perform a general task or return a result computed only from any arguments supplied by any calling code.
Static variable members can also be used for such tasks as counting class instances or may be used to provide constant values used by class instances.
Some general C++ documentation suggests that static data members are always accessible from code outside of a class even if declared “private” in a header file but the current Arduino IDE compile process reports this as an error.
This Static Member demo starts with a header file:
#ifndef MyClass_h #define MyClass_h class MyClass { public: MyClass(int); ~MyClass(); int GetVal(); static int getCount(); private: int someVal; static int iCount; // declared but also defined in cpp file static const int mult = 3; }; #endif
The header file introduces a static int (iCount), a static const int (mult) and a static int method (getCount()).
Then there is a short code file:
#include <Arduino.h>
#include "MyClass.h"
int MyClass::iCount; // iCount needs to be defined and declared in *.h
MyClass::MyClass(int sVal) {
someVal = sVal;
iCount++;
}
MyClass::~MyClass() {
iCount--;
}
int MyClass::GetVal() {
return someVal * mult;
}
static int MyClass::getCount() {
return iCount;
}
The class constructor increments the static member iCount and the destructor decrements the value. The static member is addressed in just the same way as an instance data member.
The following demo program then exercises the static members and shows that the class destructor is called when a class goes out of scope. If you run it, the MyClass::getCount() static method keeps track.
#include "MyClass.h"
#define _NL "\n"
template<class T> inline Print &operator <<(Print &obj, T arg) { obj.print(arg); return obj; }
void setup() {
Serial.begin(115200);
MyClass class1(2);
MyClass class2(4);
Serial << "There are " << MyClass::getCount() << " instances" << _NL;
addAnInstance();
Serial << "and now " << MyClass::getCount() << " instances" << _NL;
Serial << "Using Static Const " << class1.GetVal() << _NL;
}
void addAnInstance() {
MyClass class3(6);
Serial<< "There are now "<< MyClass::getCount()<< " instances"<< _NL;
}
How is a class instance implemented?
Before investigating the sometimes confusing world of pointers to class instance members we should take in an overview of how class instances are implemented. Hopefully, this will bring clarity to those pointers and we C programmers just love our pointers.
Even when there are many instances of a given class, there is only one copy of the code base for the class methods. The instances are represented by something pretty well indistinguishable from a struct that contains the class data members holding the specific member values for each instance. Any static data members are stored separately as only one copy of this set is required. When a class method is called, it has to be passed an implicit additional argument – a pointer to the class data instance. You can, in fact, use this additional pointer in your code. The pointer is called “this” and points to the current class instance. You may recall that we met the pointer named “this” before when adding an operator to a struct.
Static methods do not need the additional parameter (the pointer that is called “this”) required by instance methods. You can therefore create a normal function pointer for a static method but creating pointers to instance methods requires making provision for that additional requirement.
You can go back and insert the following two lines of code at the bottom of the setup() method in the static method demo we just looked at.
int (*statPtr)() = &MyClass::getCount; Serial << "Pointer to Static Method gets: " << statPtr() << " instances" << _NL;
As you can see, creating a pointer to a static method is just the same as creating a pointer to any function in your regular code base as no additional parameter is required, we just have to clearly identify the method. Give the revised program a run and check that all is well.
If you are interested in exploring classes in memory and have a spare 10 minutes, why not go back to that program and use sizeof() to get the size of a class and send the result to the Serial Monitor. Then you could check out the addresses of individual class instances. You will find that a global class instance and static data members are both located in the global variable space (which makes sense) while class instances created in functions (like setup()) sit high in memory on the stack like other variables created within a function.
Class Instance Member Pointers
Let’s kick off with a class header file and code to demonstrate pointers to instance methods. The class is similar to, but differs from, the one used to explore static members.
#ifndef MyClass_h #define MyClass_h class MyClass { public: MyClass(int); int GetVal(); int MyClass::SquareIt(int v); private: int someVal; }; #endif
and the class C++ code:
#include <Arduino.h> #include "MyClass.h" MyClass::MyClass(int sVal) { someVal = sVal; } int MyClass::GetVal() { return someVal; } int MyClass::SquareIt(int v) { return v * v; }
The simplest way to grab a pointer that can be used to access a class method is to get a pointer to the class instance and then use arrow notation to call the relevant class member.
Here is some initial program code that does just that.
#include "MyClass.h"
#define _NL "\n"
template<class T> inline Print &operator <<(Print &obj, T arg) { obj.print(arg); return obj; }
MyClass class1(42);
MyClass *classPointer;
void setup() {
Serial.begin(115200);
classPointer = &class1;
int res = classPointer->GetVal();
Serial << "Using class Pointer only: " << res << _NL;
}
As you can see, the classPointer value points to the address of the class instance class1. Using the normal arrow notation makes accessing the GetVal() method very straightforward. Indeed, this approach may meet a great many real world requirements.
However we can create a generic pointer to the GetVal() method by adding the following line to the setup() function. Notice that the pointer relates to “MyClass” and is not (yet) related to any instance.
int (MyClass::*funPtr)() = &MyClass::GetVal;
This can then be used with “dot” notation to use the GetVal() method for a specific class instance
int res2 = (class1.*funPtr)();
or we could dereference the class original instance pointer and use this
int res3 = (*classPointer.*funPtr)();
or use arrow notation and the class instance pointer
int res3a = (classPointer->*funPtr)();
My complete setup() code to test those options read:
void setup() { Serial.begin(115200); classPointer = &class1; int res = classPointer->GetVal(); Serial << "Using class Pointer only: " << res << _NL; // now create pointer to GetVal() class member int (MyClass::*funPtr)() = &MyClass::GetVal; // now use that pointer with a class instance int res2 = (class1.*funPtr)(); Serial << "Using function pointer: " << res2 << _NL; // now use the class pointer and member pointer together int res3 = (*classPointer.*funPtr)(); Serial << "Using dereferenced class pointer and function pointer: " << res3 << _NL; int res3a = (classPointer->*funPtr)(); Serial << "Using class pointer and function pointer variant: " << res3a << _NL; // now try a function with an argument/parameter int (MyClass::*anaPtr)(int) = &MyClass::SquareIt; int res4 = (class1.*anaPtr)(12); Serial << "Using function pointer with parameter: " << res4 << _NL; int res5 = (*classPointer.*anaPtr)(11); Serial << "Using class and function pointer with parameter: " << res5 << _NL; MyClass class2(3); int res6 = (class2.*funPtr)(); Serial << "Using function pointer to class2: " << res6 << _NL; // now iterating over an array of class instance pointers MyClass *cPointers[] = {&class1, &class2}; for(int i = 0; i < (sizeof(cPointers)/sizeof(cPointers[0])); i++) { int res7 = (cPointers[i]->*funPtr)(); Serial << "iterating class instance members: " << res7 << _NL; } }
All those approaches, exercised a lot of the options but still felt a little bit short. Suppose we wanted to arbitrarily switch between class instances AND between class methods. To investigate, I added another public method to the class which (like GetVal()) took no arguments and returned an int value. [If you are following this with code then don’t forget the addition to the header file.]
int MyClass::GetDoubleVal() {
return someVal << 1;
}
The following lines of code from the tests shown above suggested that we can use pointers to switch classes and methods around at will.
int (MyClass::*arrPtr[])()={&MyClass::GetVal, &MyClass::GetDoubleVal}; for(int i = 0; i < (sizeof(cPointers) / sizeof(cPointers[0])); i++) { for(int j = 0; j < 2; j++) { int res8 = (cPointers[i]->*arrPtr[j])(); Serial << "Iterating both classes and members: " << res8 << _NL; } }
That code is using the array of class instance pointers from above and then the added array of pointers to class instance members that return an int and take no arguments. The code then iterates over both arrays. This confirms that it is possible to access arbitrary methods at runtime using pointers. If your head is not aching too much you might agree that this at the very least shows great flexibility.