Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

6.4 Modern C++

Examine the following section of code, where we pass an object to a function by value as follows:

Student function(Student a) {
// do something
return a;
}
main() {
Student x;
Student y = function(x);
}

When function() is called, the Student object x is passed by value, which means a new object a (local to the function) is created using the Student class’s copy constructor. When a is then returned by value from the function, a second temporary object is constructed to hold the returned value. This process can involve significant computational overhead, especially for classes that manage large resources.

Move semantics allow data to be transferred without performing an expensive copy. Instead, ownership of the underlying resources is moved, leaving the source object in a valid but unspecified state. Move semantics are implemented using rvalue references, which have the following syntax:

Student& p; // Typical lvalue reference (refers to an existing object)
Student&& p; // New rvalue reference (refers to a temporary object)

The key difference between an lvalue reference and an rvalue reference is that rvalue references allow moves (i.e., transfer of resources), whereas lvalue references do not. An rvalue reference can bind to temporary objects, enabling efficient resource transfer without unnecessary copying.

The STL provides utility functions like std::move() and std::swap() that take advantage of rvalue references:

std::swap(a, b); // Efficient swaps two objects using move semantics internally
T new_obj = std::move(old_obj); // Transfers ownership from old_obj to new_obj

To fully support move semantics in your own class, you need to implement a move constructor. If no move constructor is defined, the copy constructor will be used instead, resulting in a potentially expensive deep copy.

A proper move constructor typically looks like this:

Student(Student&& source) noexcept {
// Transfer ownership of resources from 'source' to 'this'
}

The noexcept specifier is recommended (and often required by STL containers) to guarantee that the move operation will not throw exceptions. This allows the standard library to safely optimise certain operations (e.g., vector resizing). See moveExample.cpp:

#include <iostream>
#include <memory> // required for smart pointers
#include <string>
#include <utility> // required for move and swap
using namespace std;
class Student {
private:
string name;
int id;
public:
Student(string name, int id): name(name), id(id) {
cout << "Constructor called" << endl; }
Student(Student &&) noexcept;
virtual void display();
string getName() const { return name; }
int getID() const { return id; }
~Student() { cout << "Destructor called for " << name << endl; }
};
Student::Student(Student && source) noexcept { //Move constructor
cout << "Move constructor called " << endl;
name = std::move(source.name);
id = source.id;
}
void Student::display() {
cout << "A student with name " << name << " and ID " << id << endl;
}
int main() {
Student a = Student("Derek", 1234);
Student b = move(a);
b.display();
cout << "End of main()" << endl;
}

When we execute this code you will see the output:

Constructor called
Move constructor called
A student with name Derek and ID 1234
End of main()
Destructor called for Derek
Destructor called for

Interestingly, there was only ever one call to the Student constructor, despite it being passed to the move() function by value. If we examine the STL move function you will see the following definition, which includes the use of the rvalue:

std::move(_Tp && _T)

It is also possible to add an assignment operator that uses the move semantic, so that a call to:

c = move(b);

Would use the assignment move operator. The operator for the student class would have the following format:

class Student {
private:
string name;
int id;
public:
...
Student(Student &&) noexcept;
Student & operator = (Student &&) noexcept;
...
};
Student::Student(Student && source) noexcept { //Move constructor
cout << "Move constructor called " << endl;
name = std::move(source.name);
id = source.id;
}
// Move assignment operator.
Student & Student::operator = (Student && source) noexcept {
cout << "Move assignment operator performed " << endl;
name = std::move(source.name);
id = source.id;
return *this;
}
void Student::display() {
cout << "A student with name " << name << " and ID " << id << endl;
}
int main() {
Student a = Student("Derek", 1234);
Student b = move(a);
Student c = Student("Joe", 1235);
c = move(b);
c.display();
cout << "End of main()" << endl;
}

Giving the output:

Constructor called
Move constructor called
Constructor called
Move assignment operator performed
A student with name Derek and ID 1234
End of main()
Destructor called for Derek
Destructor called for
Destructor called for

Here is another stripped down example of the move constructor in use: (see moveStripped.cpp:)

#include <iostream>
#include <vector>
class MyClass {
public:
std::vector<int> data;
// Constructor
MyClass(size_t size) : data(size) {
std::cout << "Constructor called\n";
}
// Move constructor
MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
std::cout << "Move constructor called\n";
}
};
int main() {
MyClass obj1(100); // Normal constructor
MyClass obj2(std::move(obj1)); // Move constructor
return 0;
}

In this case MyClass obj1(100); creates an object with a vector of size 100 using the regular constructor. MyClass obj2(std::move(obj1)); transfers the ownership of resources from obj1 to obj2 using the move constructor. The std::move function converts obj1 into an rvalue reference, signalling that its resources can be moved. This will give the output:

Constructor called
Move constructor called

Move semantics allow efficient transfer of resources (such as memory) from one object to another without the overhead of copying. This is particularly valuable when working with large data structures or resource-intensive objects where copying would be costly.

Stop Sign

Figure 1. AI (Work Chronicles)

When an object is moved, its internal resources are transferred, not duplicated. The object being moved from is left in a valid but unspecified state. This transfer is managed through move constructors and move assignment operators.

Move semantics typically rely on rvalue references (&&), which bind to temporary objects that can safely be “moved from.” This avoids expensive deep copies and significantly improves performance, especially in high-performance or resource-constrained applications.

Introduced in C++11, a lambda function (or lambda expression) is an anonymous function (meaning it has no explicit name) that can be defined directly at the point where it is needed. This allows for concise, inline definitions of short functions, often used as callbacks, predicates, or arguments to algorithms.

A key feature of lambda functions is their ability to capture variables from the surrounding scope. This allows the lambda to access and operate on variables that are otherwise outside its local scope, making them highly flexible and convenient for many programming tasks.

If you have a function that you only need to use once, you might just use the code inline where it is called.

Take for example a simple functor example:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct display {
void operator()(int x){
cout << "[" << x << "]";
}
};
int main() {
vector<int> v {10, 50, 90, 20, 30};
std::for_each(v.begin(),v.end(), display());
}

This will give the following output:

[10][50][90][20][30]

Suppose that the function object is only ever used once. We can use an anonymous lambda function instead, which would have the form (see lambda1.cpp):

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v {10, 50, 90, 20, 30};
std::for_each(v.begin(),v.end(), [](int x) { cout << "[" << x << "]"; } );
}

Which gives the exact same output. At their simplest, lambda functions are largely anonymous function objects/functors and the syntax is neater, particularly if the functor is only ever to be used once.

One nice feature of lambda functions is that they can capture variables that are outside of the lambda function. For example, let’s say that our lambda function must multiply every value by a variable factor z. You cannot just write:

int main() {
vector<int> v {10, 50, 90, 20, 30};
int z = 3;
std::for_each(v.begin(),v.end(), [](int x) { cout << "[" << x*z << "]"; } );
}

As, z is outside the lambda function — i.e., it is not captured. Instead you must list in the capture (the []) any that are to be captured. So the final version would look like this:

int main() {
vector<int> v {10, 50, 90, 20, 30};
int z = 3;
std::for_each(v.begin(),v.end(), [z](int x) { cout << "[" << x*z << "]"; } );
}

And would give the output:

[30][150][270][60][90]

You can capture variables in a lambda either by value or by reference. The capture syntax controls how external variables are made accessible inside the lambda:

  • [z] captures z by value.
  • [&z] captures z by reference.
  • [&] captures all variables used in the lambda by reference.
  • [=] captures all variables used in the lambda by value.
  • [&, z] captures all variables by reference, except z, which is captured by value.
  • [&x, z] captures x by reference and z by value.

For example (See lambda2.cpp):

int main() {
vector<int> v {10, 50, 90, 20, 30};
int z = 3;
int sum = 0;
std::for_each(v.begin(),v.end(), [&](int x) { cout << "[" << x*z << "]";
sum+=x*z;});
cout << "The sum is " << sum << endl;
}

Gives the output:

[30][150][270][60][90]The sum is 600

If the function was to return a value you can define this using a -> notation. For example, if the lambda function above returned an int, it would have the syntax:

[z](int x)->int {
cout << "[" << x*z << "]";
return x*z; }

Clearly this is not usually required for the for_each call in this case.

As previously discussed, C++ extends the capabilities of C by introducing Object-Oriented Programming features, alongside modern memory management mechanisms such as smart pointers, establishing itself as a powerful and popular choice for contemporary embedded systems.

Section titled “Navigating Performance and Memory Trade-offs”

While C++ offers substantial benefits, certain OOP features, if not managed carefully, can introduce performance or memory overheads in resource-constrained embedded environments.

Virtual Functions (Dynamic Polymorphism)
These enable runtime polymorphism by employing “runtime binding” through vtables (arrays of function pointers per class) and vpointers (hidden members pointing to the vtable). While powerful for design flexibility, they can incur a slight runtime overhead due to indirect memory accesses (potential cache misses) and may hinder certain compiler optimisations like inlining.

A common concern regarding virtual function overhead in embedded systems is often overstated for modern microcontrollers. While a measurable performance difference exists (e.g., of the order of 5-10 nanoseconds per call), this overhead is typically negligible unless the functions are called millions of times per second within tight, performance-critical loops. Modern microcontrollers possess significant processing power, and the design benefits of OOP, such as modularity, reusability, and flexible interfaces, frequently outweigh these micro-optimisations. The core principle for embedded C++ developers is to profile their applications to identify actual performance bottlenecks rather than preemptively avoiding features based on theoretical overhead.

Often, the root cause of performance issues lies in inefficient algorithms or I/O operations, not in the use of virtual calls. The “overhead” of virtual functions is a relative concept; for powerful ‘smart’ edge nodes, the CPU cycles expended on a vtable lookup are often insignificant when weighed against the overall complexity managed by OOP. The design flexibility afforded by polymorphism (e.g., the ability to easily swap out different sensor types or communication interfaces) provides a net gain in development efficiency, maintainability, and adaptability. The pragmatic guideline, “It’s not a performance problem until you can prove it,” is crucial here, encouraging empirical analysis over dogmatic avoidance. This perspective suggests that for modern edge systems, the design flexibility offered by virtual functions frequently provides greater value than their minimal runtime cost.

Stop Sign

Figure 2. Imposter Syndrome (Work Chronicles)

Run-Time Type Information (RTTI)
Features such as typeid() and dynamic_cast facilitate runtime identification of object types. While typeid() calls can be fast and utilise negligible memory space, dynamic_cast is generally more computationally expensive as it involves traversing the inheritance tree. RTTI, in general, can contribute to increased binary size and runtime overhead. Similar to virtual functions, the impact of RTTI is often minor unless it is used excessively within performance-critical inner loops. For embedded systems, RTTI and exception handling are frequently compiler-configurable features that can be disabled if not strictly required, allowing developers to fine-tune the system for specific constraints and optimise for size and speed. This highlights a pragmatic approach to C++ in embedded systems: leveraging its power while carefully managing features that might introduce unnecessary overhead for a given application.

Dynamic Memory Allocation (DMA)
While dynamic memory allocation (using new/delete in C++ or malloc/free in C) offers flexibility for handling unknown data sizes and growing data structures, such as linked lists or network buffers, its use in embedded systems, particularly for long-running applications, carries significant risks. The primary concern is memory fragmentation, which can lead to wasted memory, slower allocation times, and unpredictable out-of-memory errors. Dynamic memory allocation is a nuanced tool in embedded systems. For complex edge applications that must handle variable data (e.g., JSON payloads from network requests), it offers essential flexibility that static allocation cannot provide. However, its unpredictable nature, including fragmentation and potential out-of-memory crashes, makes it a high-risk feature for systems requiring sustained uptime and deterministic behaviour. This necessitates a strategic approach: prioritising static allocation or fixed-size buffers whenever possible, and when DMA is unavoidable (e.g., for network stacks or variable message queues), employing controlled allocation strategies such as memory pools or custom allocators.

The Embedded Template Library (ETL) is a C++ alternative that provides standard containers without dynamic allocation**, offering a safer middle ground for managing memory in constrained environments. For powerful edge nodes running Embedded Linux, the need for dynamic memory allocation increases due to the complexity of applications like network protocols and data processing. However, the inherent risks associated with it persist. This creates a tension between the desire for flexibility and the requirement for determinism. Developers must move beyond a blanket “DMA is bad” to a more sophisticated understanding that “DMA must be managed strategically.” This involves choosing controlled allocation patterns or using specialised libraries to mitigate fragmentation and ensure reliability, even on more capable hardware.

The following table summarises key C++ OOP features and their considerations for embedded systems:

Table 2: C++ OOP Feature Considerations for Embedded Systems

FeatureCore OOP BenefitPotential Embedded Overhead/RiskMitigation/Best Practice in Embedded
Virtual FunctionsDynamic Polymorphism, Flexible InterfacesRuntime overhead (vtable lookup), Potential cache misses, Hinders inliningProfiling to identify actual bottlenecks, Judicious use, Consider static polymorphism (CRTP) if performance is critical
Run-Time Type Information (RTTI)Runtime Type Identification (typeid, dynamic_cast)Increased binary size, Runtime overhead (especially dynamic_cast)Disable via compiler flags if not strictly needed, Avoid in performance-critical loops, Use static alternatives where possible
Dynamic Memory AllocationFlexible data structures (e.g., lists, maps), Handling unknown data sizesMemory fragmentation, Unpredictable out-of-memory errors, Slower allocationsPrioritise static allocation/fixed-sse buffers, Use memory pools/custom allocators, Employ Embedded Template Library (ETL) containers
TemplatesCode Reusability, Generic Programming, Compile-time PolymorphismPotential code bloat (monomorphisation), Longer compilation timesUse judiciously, Optimise with compiler flags, Employ type-erasure for smaller binaries

Table 3: C++ STL Algorithms Summary

CategoryAlgorithmFunctionApplicable ContainersBasic Syntax
Non-
modifying operations
for_eachApply function to each elementAny container with iteratorsstd::for_each(v.begin(), v.end(), func);
findFind first element equal to valueAny sequential containerstd::find(v.begin(), v.end(), value);
countCount occurrences of valueAny sequential containerstd::count(v.begin(), v.end(), value);
all_of  
any_of  
none_of
Test condition on rangeAny sequential containerstd::all_of(v.begin(), v.end(), pred);
equalTest equality of two rangesAny sequential containerstd::equal(v1.begin(), v1.end(), v2.begin());
Modifying operationscopyCopy elements to another containerAny container with output iteratorsstd::copy(src.begin(), src.end(), dest.begin());
fillFill container with valueAny sequential containerstd::fill(v.begin(), v.end(), value);
replaceReplace occurrences of valueAny sequential containerstd::replace(v.begin(), v.end(), old, new);
remove remove_ifRemove elements matching conditionAny sequential container (use with erase)v.erase(std::remove(v.begin(), v.end(), value), v.end());
Sorting & orderingsortSort elementsRandom access containers (vector, deque, array)std::sort(v.begin(), v.end());
stable_sortSort while preserving order of equal elementsRandom access containersstd::stable_sort(v.begin(), v.end());
reverseReverse orderAny sequential containerstd::reverse(v.begin(), v.end());
shuffleRandomly shuffle elementsRandom access containersstd::shuffle(v.begin(), v.end(), rng);
Searching (sorted)binary_searchCheck if value exists (sorted)Random access containersstd::binary_search(v.begin(), v.end(), value);
lower_bound  upper_boundFind range for valueRandom access containersstd::lower_bound(v.begin(), v.end(), value);
Numeric algorithmsaccumulateSum (or combine) elementsContainers of numeric typesstd::accumulate(v.begin(), v.end(), init);
inner_productCompute inner productContainers of numeric typesstd::inner_product(a.begin(), a.end(), b.begin(), init);
Set operations (sorted containers)set_unionUnion of two setsSorted containers (set, vector)std::set_union(a.begin(), a.end(), b.begin(), b.end(), dest.begin());
set_intersectionIntersection of two setsSorted containersstd::set_intersection(a.begin(), a.end(), b.begin(), b.end(), dest.begin());
set_differenceDifference of two setsSorted containersstd::set_difference(a.begin(), a.end(), b.begin(), b.end(), dest.begin());
Heap operationsmake_heap  push_heap  pop_heap  sort_heapHeap manipulationRandom access containersstd::make_heap(v.begin(), v.end());
Min/maxmin_element  max_elementFind min/max elementAny sequential containerstd::min_element(v.begin(), v.end());
min 
max
Return min/max of two valuesAny comparable typesstd::min(a, b);