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;
}
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 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 move_example.cpp:

move_example.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:

student_move_example.cpp
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 move_stripped.cpp:)

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;
}
Stop Sign

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 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.

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.

Concept Match

Match the Move Semantics Terminology

Code Cloze
C++

Implementing a Move Constructor

Quiz
Select 0/1

What is the primary performance benefit of using move semantics over traditional deep copying?

Quiz
Select 0/1

Why is it highly recommended to mark a move constructor with the 'noexcept' specifier?

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:

lambda_1.cpp
#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):

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] 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 lambda_3.cpp):

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 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.

Code Cloze
C++

Lambda Capture Lists

Quiz
Select 0/1

What does the capture list '[=]' signify in a C++ lambda function?

Quiz
Select 0/1

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>

The following example models a temperature sensor that may be unavailable. When available, it returns a float reading; otherwise it returns an empty optional:

optional_example.cpp
#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 C
Reading (with fallback): -999

The * 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:

optional_edge.cpp
#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 failure

Notice 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.

MethodDescription
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

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>
IntroducedC++17Core language
Empty sentinelstd::nulloptNone
Value wrapperDirect (no heap)Direct (no heap)
Pattern matchingif/value_or()match, if let, ?
Compiler enforcementWarns 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.

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.

Concept Match

Match the Error Handling Concepts

Quiz
Select 0/1

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>

Here is an example usage:

string_view_example.cpp
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// Takes any string-like input with no heap allocation
void 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.25

string_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.

MethodDescription
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

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.

Quiz
Select 0/1

What is the primary advantage of using std::string_view over const std::string& as a function parameter?

Quiz
Select 0/1

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:

static_reflection.cpp
#include <meta>
#include <iostream>
using namespace std;
struct SensorReading {
float temperature;
float humidity;
int id;
};
// Count members at compile time
constexpr size_t n = std::meta::members_of(^^SensorReading).size(); // 3

Why does this matter for edge programming? Many common edge tasks are pure boilerplate today:

  • Serialising a SensorReading struct 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.

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:

embed_preprocessor.cpp
// 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 connections
const 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:

Terminal window
xxd -i root_ca.pem > root_ca_gen.h # generates a uint8_t[] initialiser

The 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:

embed_.cpp
#include <cstddef>
// Embed a binary calibration file produced by the hardware test rig
constexpr 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.

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-size 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
Quiz
Select 0/1

What is the pragmatic approach recommended for handling the overhead of virtual functions in modern embedded C++ development?

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);
transformApply function to each element, writing result to output rangeAny sequential containerstd::transform(v.begin(), v.end(), out.begin(), func);
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);