6.4 Modern C++

Move Semantics (Advanced)
Section titled “Move Semantics (Advanced)”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 internallyT new_obj = std::move(old_obj); // Transfers ownership from old_obj to new_objTo 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 swapusing 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 calledMove constructor calledA student with name Derek and ID 1234End of main()Destructor called for DerekDestructor called forInterestingly, 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 calledMove constructor calledConstructor calledMove assignment operator performedA student with name Derek and ID 1234End of main()Destructor called for DerekDestructor called forDestructor called forHere 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 calledMove constructor calledMove 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.

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.
Lambda Functions/Expression
Section titled “Lambda Functions/Expression”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]captureszby value.[&z]captureszby 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, exceptz, which is captured by value.[&x, z]capturesxby reference andzby 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 600If 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.
C++ and OOP in Smart Edge Environments
Section titled “C++ and OOP in Smart Edge Environments”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.
Navigating Performance and Memory Trade-offs
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.

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
| Feature | Core OOP Benefit | Potential Embedded Overhead/Risk | Mitigation/Best Practice in Embedded |
| Virtual Functions | Dynamic Polymorphism, Flexible Interfaces | Runtime overhead (vtable lookup), Potential cache misses, Hinders inlining | Profiling 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 Allocation | Flexible data structures (e.g., lists, maps), Handling unknown data sizes | Memory fragmentation, Unpredictable out-of-memory errors, Slower allocations | Prioritise static allocation/fixed-sse buffers, Use memory pools/custom allocators, Employ Embedded Template Library (ETL) containers |
| Templates | Code Reusability, Generic Programming, Compile-time Polymorphism | Potential code bloat (monomorphisation), Longer compilation times | Use judiciously, Optimise with compiler flags, Employ type-erasure for smaller binaries |
Appendix
Section titled “Appendix”Table 3: C++ STL Algorithms Summary
| Category | Algorithm | Function | Applicable Containers | Basic Syntax |
| Non- modifying operations | for_each | Apply function to each element | Any container with iterators | std::for_each(v.begin(), v.end(), func); |
| find | Find first element equal to value | Any sequential container | std::find(v.begin(), v.end(), value); | |
| count | Count occurrences of value | Any sequential container | std::count(v.begin(), v.end(), value); | |
| all_of any_of none_of | Test condition on range | Any sequential container | std::all_of(v.begin(), v.end(), pred); | |
| equal | Test equality of two ranges | Any sequential container | std::equal(v1.begin(), v1.end(), v2.begin()); | |
| Modifying operations | copy | Copy elements to another container | Any container with output iterators | std::copy(src.begin(), src.end(), dest.begin()); |
| fill | Fill container with value | Any sequential container | std::fill(v.begin(), v.end(), value); | |
| replace | Replace occurrences of value | Any sequential container | std::replace(v.begin(), v.end(), old, new); | |
| remove remove_if | Remove elements matching condition | Any sequential container (use with erase) | v.erase(std::remove(v.begin(), v.end(), value), v.end()); | |
| Sorting & ordering | sort | Sort elements | Random access containers (vector, deque, array) | std::sort(v.begin(), v.end()); |
| stable_sort | Sort while preserving order of equal elements | Random access containers | std::stable_sort(v.begin(), v.end()); | |
| reverse | Reverse order | Any sequential container | std::reverse(v.begin(), v.end()); | |
| shuffle | Randomly shuffle elements | Random access containers | std::shuffle(v.begin(), v.end(), rng); | |
| Searching (sorted) | binary_search | Check if value exists (sorted) | Random access containers | std::binary_search(v.begin(), v.end(), value); |
| lower_bound upper_bound | Find range for value | Random access containers | std::lower_bound(v.begin(), v.end(), value); | |
| Numeric algorithms | accumulate | Sum (or combine) elements | Containers of numeric types | std::accumulate(v.begin(), v.end(), init); |
| inner_product | Compute inner product | Containers of numeric types | std::inner_product(a.begin(), a.end(), b.begin(), init); | |
| Set operations (sorted containers) | set_union | Union of two sets | Sorted containers (set, vector) | std::set_union(a.begin(), a.end(), b.begin(), b.end(), dest.begin()); |
| set_intersection | Intersection of two sets | Sorted containers | std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), dest.begin()); | |
| set_difference | Difference of two sets | Sorted containers | std::set_difference(a.begin(), a.end(), b.begin(), b.end(), dest.begin()); | |
| Heap operations | make_heap push_heap pop_heap sort_heap | Heap manipulation | Random access containers | std::make_heap(v.begin(), v.end()); |
| Min/max | min_element max_element | Find min/max element | Any sequential container | std::min_element(v.begin(), v.end()); |
| min max | Return min/max of two values | Any comparable types | std::min(a, b); |
© 2026 Derek Molloy, Dublin City University. All rights reserved.