EmbeddedRelated.com
Blogs

C to C++: Templates and Generics – Supercharging Type Flexibility

Jacob BeningoMarch 24, 20242 comments

While beneficial to embedded developers, the C programming language can be inflexible. For example, have you ever had a function that had to manage multiple types? How did you solve that problem?

I bet you either duplicated that function, which led to confusing code or used a complex series of macros. Neither of those is terribly helpful because it creates code that is difficult to maintain and is confusing to future developers.

C++ templates and generics can come to the rescue!

This article is available in PDF format for easy printing

Note: If you are interested in catching up on this series, the previous blogs can be found at the links below:

What is a Template in C++?

A template is a blueprint for creating a class or function that can operate with any data type. Unlike C, where functions and data structures must be defined for each data type, templates in C++ allow for code reuse with different data types without multiple codebases.

For example, let’s say that you have a simple adder function. In C, the adder function might look something like:

uint8_t add(uint8_t a, uint8_t b)
{
    return a + b;
}

As mentioned earlier, if I need add to work with uint8_t, uint16_t, and uint32_t types, I need to create individual functions for each.

In C++, I can use a template. The syntax for using a template will look something like the following:

template <typename T>
T add(T a, T b) 
{
    return a + b;
}

Before our function, we use the keyword template, followed by the type name definition of T. T is used throughout our function to tell the C++ compiler that whatever type we are using should be substituted for T.

Using C++ Templates

Let’s look at an example. Once again, let’s look at a simple adder function. You already have a C++ template function for add that we defined above. Let’s write a simple application that uses this template:

#include <cstdint>
#include <iostream>
template <typename T>
T add(T a, T b) 
{
    return a + b;
}
int main() 
{
    uint8_t num1 = 100;       // Example value for num1
    uint8_t num2 = 28;        // Example value for num2
    // Call the 'add' function with uint8_t arguments
    uint8_t sum = add(num1, num2);
    std::cout << "The sum is: " << static_cast<int>(sum) << std::endl;
    return 0;
}

In the application above, we aren’t explicitly stating the type that add needs to be. We allow the compiler to generate the correct code during compile time so we can manage the add function. If we later updated our application to use a uint16_t, we would update the application to the following:

#include <cstdint>
#include <iostream>
template <typename T>
T add(T a, T b) 
{
    return a + b;
}
int main() 
{
    uint16_t num1 = 100;      // Example value for num1
    uint16_t num2 = 28;        // Example value for num2
    // Call the 'add' function with uint8_t arguments
    uint16_t sum = add(num1, num2);
    std::cout << "The sum is: " << static_cast<int>(sum) << std::endl;
    return 0;
}

Explicitly using Templates

As I mentioned earlier, the application we’ve looked at implicitly sets the template data type. Basically, we are letting the compiler do the work for us. Depending on your school of development, you may or may not like this. Many developers want to specify the type explicitly. For example, we would change the original code:

uint8_t sum = add(num1, num2);

to

uint8_t sum = add<uint8_t>(num1, num2);

The add function is called in this code with an explicit template argument: add(num1, num2);. This explicitly tells the compiler that you want to use the uint8_t specialization of the add template. This can be particularly useful if the type deduction might be ambiguous or if you want to ensure the exact type to be used in the operation.

What is a Generic in C++?

A generic is a component that is designed to operate with any data type. It's a way to write flexible, reusable code without committing to a specific type until the code is actually used or instantiated. This is highly beneficial in embedded systems, where the same operations might be performed across different data types but within resource constraints that demand minimal code duplication.

In C++, generics are primarily achieved through templates, allowing developers to write generic and reusable code.

Consider an embedded system responsible for sensor data acquisition. Different sensors might yield different data types; for instance, a temperature sensor might output float values, while a position sensor might output int for coordinates. Without generics, you would need separate functions for each data type.

Instead, you can use templates to create a generic class that can operate on multiple data types like the following:

template <typename T>
class SensorData 
{
public:
    T data;
    SensorData(T value) : data(value) {}
    void logData() const 
    {
        // Log the data to some memory or output interface
        // The implementation will be the same regardless of the type of data
    }
    T getData() const 
    {
        return data;
    }
};

As you can see, we are using templates here to create a generic object that could operate on floats, ints, or several other datatypes. If we were to use them in an application, we could do something like the following:

int main() 
{
    SensorData<float> temperatureSensor(36.5f);
    SensorData<int> positionSensor(1024);
    temperatureSensor.logData();
    positionSensor.logData();
}

We are using two different instances of the SensorData class here; one of type float and the other type int.

Type Checking Templates and Generics with static_assert 

If you think back to our earlier add template, you may have noticed a slight problem. You might have been thinking about add strictly from an arithmetic standpoint! For example, you might expect a and b to be arithmetic types rather than, say, a string type. If you were to pass two string variables into add, you might get unexpected results.

So, how can you ensure that anyone using the add template is passing arithmetic types into add?

The answer is that you can use static_assert and type-checking built into the standard library.

static_assert is a feature introduced in C++11 that allows you to perform compile-time assertions. This means that you can use static_assert to check if certain conditions are true while your code is being compiled rather than at runtime. If the condition evaluated by static_assert is false, the compilation will stop, and the compiler will display a message that you can define.

The static_assert declaration is followed by a constant expression that can be evaluated at compile time and, optionally, a string literal to display as a diagnostic message if the assertion fails. For example, to ensure that add only works with arithmetic types, you might update the add template with a static assertion as follows:

template <typename T>
T add(T a, T b) 
{
    static_assert(std::is_arithmetic<T>::value, “Requires arithmetic types”);
    return a + b;
}

Now, if you try to do something fun like:

string add<string>(“Hello “, “World”);

I’ll get a compiler error, “Requires arithmetic types”!

Can you see how powerful and flexible this code can be? Not only is our add function able to work with multiple types automatically, but we’re also able to ensure type safety!

We can extend the use to static_assert to many other code areas. It’s a bit beyond today’s scope, but here is one more example before we move on. The following is a static assertion that the int type is 4 bytes wide:

static_assert(sizeof(int) == 4, "int must be 4 bytes");

Obviously, this can be extremely useful if you expect to have 4-byte ints on, say, an Arm Cortex-M microcontroller, but you port the code to a Microchip PIC8.

Pros and Cons of Templates and Generics in Embedded Software

Templates and Generics are potent programming tools; However, you must be careful with them. Every tool has its place, and if it’s not used correctly, problems can arise.

For example, when I started in the embedded systems industry, a war raged between C and C++ advocates. Many argued that C++ was slow and bloated. Looking back, I think in many cases, C++ as a language and its best practices just weren’t well understood by embedded developers.

While we are doing a fair amount of cheerleading for C++ in these blogs, let’s try to quickly look at the pros and cons of using C++ for embedded.

Pros to using C++ in Embedded Systems

There are several critical pros to using C++ in Embedded Systems, such as:

  • Type Safety
  • Performance
  • Maintainability

Type Safety can help you to reduce runtime errors. Many issues related to types can be caught during compilation. In C, type-checking is minimal, which can lead to many software bugs. C++ improves the type-checking system to ensure that we get what we expect.

Template-generated code is specifically tailored for the given type at compile time, resulting in machine code that is as efficient as if you wrote a dedicated function or class for that specific type. This means templates incur no runtime overhead: a function template instantiated with an int will perform just as fast as a function specifically written for int. This efficiency is critical in performance-sensitive applications like embedded systems, where every cycle counts.

Code written in C++ is much more maintainable than C code. As you have seen, you can use a single template to manage multiple type-specific functions and classes. For example, in C, you’d have to write separate, duplicate functions for add to support int and float. In C++, it’s a single template. One place to change code if a change is required.

Cons to using C++ in Embedded Systems

While C++ offers us many benefits as embedded programmers, there are also some cons. For example, several cons that immediately come to mind include:

  • Code Bloat
  • Compilation Time
  • Complex Error Messages

Code bloat has been mentioned several times now in passing, but it’s time to point out why. If you instantiate the add template for float and int, the C++ compiler will generate two add functions. One will support float, and the other will support int. The code is duplicated for two different types, which one could argue bloats the code.

The above example is relatively poor because you’d have to create two different add functions anyway. The problem is when you create a template that truly creates code bloat. For example, what would happen if you created a template to manage gpio pins like the following:

volatile uint32_t* const GPIO_PORT = reinterpret_cast<uint32_t*>(0x40021000);
const uint32_t PIN_ON_MASK = 1U;
const uint32_t PIN_OFF_MASK = ~PIN_ON_MASK;
template <std::size_t PIN_NUMBER>
class GPIOPin 
{
public:
    void turnOn() const 
    {
        *GPIO_PORT |= (PIN_ON_MASK << PIN_NUMBER);
    }
    void turnOff() const 
    {
        *GPIO_PORT &= ~(PIN_OFF_MASK << PIN_NUMBER);
    }
    void toggle() const 
    {
        *GPIO_PORT ^= (PIN_ON_MASK << PIN_NUMBER);
    }
};

At first glance, this doesn’t seem too bad. I have a class to manage each GPIO pin. It’s templated for flexibility and reuse. But what happens when I instantiate my GPIO objects? If I have 128 GPIO pins and I instantiate each pin using the template class, I’ll end up with 128 copies! That will bloat my code!

In truth, that is more an issue of how I implemented my code rather than C++ being bloated. However, misunderstandings like that can quickly get into your code and bloat the code if you aren’t careful.

Another con is that compilation times may go up often. There are a few reasons for these like:

  • Type checking
  • Running static_assert
  • Generating template code

For embedded developers, we’d rather have longer compilation times and faster, more deterministic runtime. It would be preferred that our code be checked for safety during compile time. So, while this is often listed as a con, it’s one that we can readily live with.

The bottom line with C++ Templates

Templates and generics are potent tools in C++ that allow developers, especially in the embedded domain, to write flexible, type-safe, and efficient code. While there are trade-offs, such as potential code bloat and increased compilation time, with careful use, templates can significantly enhance code quality and maintainability.

As you begin to use templates in your embedded software, make sure you adhere to the following best practices:

  • Prefer compile-time over run-time
  • Leverage static_assert for type-checking
  • Monitor the binary size to ensure templates are not introducing code bloat.
  • Keep templates simple
  • Avoid nested templates

If you adhere to these best practices, you’ll find the advantages you get from templates will far outweigh the disadvantages.

Happy Coding!


Interested in learning more C++ for embedded systems? 

Take Jacob's on-demand course "Migrating to C++: Objects and Object-Oriented Design" and save $100 (33%) with the coupon code ER24WS100. (Note: you'll also have access to future live online sessions too!). 



[ - ]
Comment by HamiltonApril 10, 2024

The article clearly points out most of the subtleties to be taken into account when using templates. However, I've found that using templates in a hierarchical way leads to not being able to debug SW behavior properly, because for the debugger the templates become a black box.

[ - ]
Comment by beningjwApril 12, 2024

Thanks for the comment! This is a great point!

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: