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;}
int 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 move_example.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 move_stripped.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;}
Figure 1. AI (Work Chronicles)
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.
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.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Match the Move Semantics Terminology
Implementing a Move Constructor
What is the primary performance benefit of using move semantics over traditional deep copying?
Why is it highly recommended to mark a move constructor with the 'noexcept' specifier?
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 lambda_2.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 lambda_3.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.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Lambda Capture Lists
What does the capture list '[=]' signify in a C++ lambda function?
Which of the following is a primary reason for using lambda expressions in modern C++?
std::optional: Representing Values That May Be Absent (C++17)
Section titled “std::optional: Representing Values That May Be Absent (C++17)”In edge systems, many operations can legitimately fail to produce a value: a sensor may be disconnected, a CRC check may fail, or a calibration value may not yet be available. A common but fragile pattern is to use a sentinel value (e.g., -1, 0, NaN, or nullptr) to signal “no result”, which requires callers to know and check that sentinel. Another approach is to throw an exception, which carries runtime overhead and is often disabled on embedded platforms.
std::optional<T>, introduced in C++17, provides a clean, zero-overhead alternative. It is a wrapper that either holds a value of type T, or holds nothing (std::nullopt). Crucially, the absence of a value is explicit in the type, so the compiler can enforce that callers handle both cases.
#include <optional>Basic Usage
Section titled “Basic Usage”The following example models a temperature sensor that may be unavailable. When available, it returns a float reading; otherwise it returns an empty optional:
#include <iostream>#include <optional>using namespace std;
optional<float> readTemperature(bool sensorAvailable) { if (!sensorAvailable) return nullopt; // no value return 21.5f; // normal reading}
int main() { auto result = readTemperature(true); if (result.has_value()) cout << "Temperature: " << result.value() << " C" << endl; else cout << "Sensor unavailable" << endl;
auto missing = readTemperature(false); // value_or() provides a safe fallback without requiring an explicit check cout << "Reading (with fallback): " << missing.value_or(-999.0f) << endl;}Temperature: 21.5 CReading (with fallback): -999The * dereference operator can also be used as shorthand for .value(), but only after confirming the optional holds a value — dereferencing an empty optional is undefined behaviour.
Edge Sensor Example: CRC-Validated Humidity Reading
Section titled “Edge Sensor Example: CRC-Validated Humidity Reading”A more realistic edge scenario involves reading a structured sensor packet where a CRC check may reject the data. Using std::optional makes the validation outcome part of the function’s type contract:
#include <iostream>#include <optional>#include <cstdint>using namespace std;
struct HumidityReading { float humidity; float temperature;};
// Simulates reading a sensor packet and validating its CRC.// Returns a valid reading only if the CRC passes.optional<HumidityReading> readHumiditySensor(bool crcValid) { if (!crcValid) return nullopt; return HumidityReading{58.3f, 22.1f};}
void processSensorData(bool crcOk) { auto reading = readHumiditySensor(crcOk); if (reading) { // operator bool() equivalent to has_value() cout << "Humidity: " << reading->humidity << " %" << endl; cout << "Temperature: " << reading->temperature << " C" << endl; } else { cout << "Packet discarded: CRC failure" << endl; }}
int main() { cout << "--- Valid packet ---" << endl; processSensorData(true); cout << "--- Corrupt packet ---" << endl; processSensorData(false);}--- Valid packet ---Humidity: 58.3 %Temperature: 22.1 C--- Corrupt packet ---Packet discarded: CRC failureNotice the -> operator used on the optional (reading->humidity): when an optional holds a struct, -> accesses the struct’s members directly, making the code concise and clear.
Key Methods
Section titled “Key Methods”| Method | Description |
|---|---|
has_value() | Returns true if a value is present |
operator bool() | Same as has_value(); allows if (opt) idiom |
value() | Returns the stored value; throws std::bad_optional_access if empty |
value_or(default) | Returns the value or a fallback default if empty |
operator* / operator-> | Direct access (undefined behaviour if empty; check first) |
reset() | Discards the held value, making the optional empty |
Comparison with Rust’s Option<T>
Section titled “Comparison with Rust’s Option<T>”Readers of Chapter 8 will recognise std::optional as C++‘s equivalent of Rust’s Option<T>. The concepts are the same — wrapping a value that may or may not exist — but there are practical differences:
C++ std::optional<T> | Rust Option<T> | |
|---|---|---|
| Introduced | C++17 | Core language |
| Empty sentinel | std::nullopt | None |
| Value wrapper | Direct (no heap) | Direct (no heap) |
| Pattern matching | if/value_or() | match, if let, ? |
| Compiler enforcement | Warns if unchecked (with -Wall) | Exhaustive match required |
Rust enforces exhaustive handling at compile time, whereas C++ relies on programmer discipline. In C++, value() will throw if the optional is empty — but exception handling is often disabled on embedded platforms. Prefer value_or() or an explicit has_value() check for safety in edge code.
Edge Programming Considerations
Section titled “Edge Programming Considerations”std::optional is zero-overhead in terms of heap allocation: the value (if present) is stored inline within the optional object, with only a single boolean flag added. For a float reading, the optional is typically 8 bytes on a 32-bit system. This makes it well-suited to resource-constrained edge nodes.
However, it does increase stack frame size, so avoid holding large structs (or arrays) inside an optional in deeply nested call stacks on devices with limited stack space. For those cases, consider returning a status code alongside an output reference parameter, or using a small fixed-size structure with an explicit valid flag.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Match the Error Handling Concepts
Why is std::optional generally considered 'zero-overhead' for edge devices compared to traditional dynamic memory approaches?
std::string_view: Zero-Copy String Handling (C++17)
Section titled “std::string_view: Zero-Copy String Handling (C++17)”In edge code, strings appear everywhere: sensor labels, configuration keys, protocol messages, log entries. The traditional approach (e.g., passing std::string by value or const std::string&) has a hidden cost. When a function receives a const std::string&, it must already have a std::string object: passing a string literal like "temperature" forces the compiler to construct a temporary std::string, heap-allocating a copy of the characters.
std::string_view, introduced in C++17, solves this. It is a lightweight, non-owning view over any contiguous sequence of characters — i.e., a string literal, a std::string, or a raw buffer. No heap allocation, no copy.
#include <string_view>Basic Usage
Section titled “Basic Usage”Here is an example usage:
#include <iostream>#include <string>#include <string_view>using namespace std;
// Takes any string-like input with no heap allocationvoid logSensor(string_view label, float value) { cout << "[SENSOR] " << label << " = " << value << endl;}
int main() { logSensor("temperature", 23.7f); // string literal — no allocation logSensor("humidity", 58.1f); // string literal — no allocation
string key = "pressure"; logSensor(key, 1013.25f); // std::string — no copy needed}[SENSOR] temperature = 23.7[SENSOR] humidity = 58.1[SENSOR] pressure = 1013.25string_view is particularly valuable in parsing and configuration code where the same character buffer is sliced into many sub-strings. Returning string_view sub-ranges avoids allocating a new std::string for each piece.
Key Methods
Section titled “Key Methods”| Method | Description |
|---|---|
data() | Returns a pointer to the first character (not null-terminated) |
size() / length() | Number of characters |
substr(pos, len) | Returns a sub-view (no allocation) |
find(str) | First position of substring |
starts_with(str) | C++20: true if the view starts with the given prefix |
remove_prefix(n) | Advances the start by n characters |
Important Caveats
Section titled “Important Caveats”string_view does not own the characters it views. You must ensure the underlying data outlives the view:
string_view danger() { string s = "hello"; return s; // DANGER: returns a view into a local that is about to be destroyed}Never store a string_view that outlives its source string. Use it as a function parameter or short-lived local; convert to std::string if you need to store the value beyond the current scope.
🧩Knowledge Check
Section titled “🧩Knowledge Check”What is the primary advantage of using std::string_view over const std::string& as a function parameter?
Why is it unsafe to return a std::string_view that refers to a local std::string variable?
Looking Ahead: C++26 Static Reflection (Advanced)
Section titled “Looking Ahead: C++26 Static Reflection (Advanced)”One of the most significant features on the horizon for C++ is static reflection, standardised in C++26. Where templates allow you to write code that is generic over types, reflection allows code to inspect types (their names, members, and annotations) at compile time, without macros or external code-generation tools.
The core mechanism is the reflection operator ^^, which produces a compile-time representation of a type. The std::meta library provides functions to query that representation:
#include <meta>#include <iostream>using namespace std;
struct SensorReading { float temperature; float humidity; int id;};
// Count members at compile timeconstexpr size_t n = std::meta::members_of(^^SensorReading).size(); // 3Why does this matter for edge programming? Many common edge tasks are pure boilerplate today:
- Serialising a
SensorReadingstruct to JSON for an MQTT payload requires hand-written code that mirrors the struct field-by-field. - Logging every member of a packet struct for debugging requires the same manual listing.
- Mapping protocol byte offsets to struct fields requires macros or external tooling.
With static reflection, a single generic function can walk any struct’s members at compile time and generate the serialisation, logging, or mapping code automatically — with no runtime overhead and no macros.
Looking Ahead: C++26 #embed (Advanced)
Section titled “Looking Ahead: C++26 #embed (Advanced)”The #embed preprocessor directive, standardised in C++26 (and simultaneously in C23), embeds the raw bytes of an external file directly into an array initialiser at compile time. It is fundamentally different from #include, which performs textual substitution: #embed always expands to a comma-separated sequence of integer byte values, regardless of the file’s format.
Here is an example of what #embed actually does:
// Note: index.html is served by an ESP32 web server.// The page is baked into the binary -- no SPIFFS, no SD card, no file I/O.const char web_page[] = { #embed "index.html" , '\0' // append a null terminator so it is a valid C string};
// TLS root certificate for secure MQTT connectionsconst unsigned char root_ca_pem[] = { #embed "root_ca.pem"};constexpr std::size_t root_ca_pem_len = sizeof(root_ca_pem);Both arrays reside in the binary’s read-only section (flash on a microcontroller). There is no runtime file I/O and no file-system dependency. If index.html or root_ca.pem does not exist at build time the compiler reports a missing-file error, catching the problem during development rather than as a runtime crash in the field.
Embedding a binary file previously required an external tool, such as:
xxd -i root_ca.pem > root_ca_gen.h # generates a uint8_t[] initialiserThe generated file had to be committed to version control, re-run every time the source changed, and the regeneration step had to be wired into the build system by hand. #embed replaces the entire workflow with a single standard directive that the compiler handles automatically on every build.
The #embed preprocessor directive supplies the raw bytes. The processing of those bytes (i.e., parsing, validating, transforming) is performed by separate constexpr or consteval code that operates on the embedded data during compilation. The two mechanisms complement each other:
#include <cstddef>
// Embed a binary calibration file produced by the hardware test rigconstexpr unsigned char sensor_cal[] = { #embed "sensor_cal.bin"};
// This sensor model's calibration file must be exactly 12 bytes:// bytes 0-3 : offset (float, little-endian)// bytes 4-7 : scale (float, little-endian)// bytes 8-11 : CRC-32 (uint32_t, little-endian)static_assert(sizeof(sensor_cal) == 12, "sensor_cal.bin: expected 12 bytes -- rebuild from the calibration rig");The static_assert turns a wrongly-sized calibration file into a build error. A device will never leave the factory with a calibration table of the wrong size because the compiler refuses to produce a binary.
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-size 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 |
🧩Knowledge Check
Section titled “🧩Knowledge Check”What is the pragmatic approach recommended for handling the overhead of virtual functions in modern embedded C++ development?
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); | |
| transform | Apply function to each element, writing result to output range | Any sequential container | std::transform(v.begin(), v.end(), out.begin(), func); | |
| 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.