The auto
keyword was introduced with C++11. It reduces the amount of code you have to write, reduces repetitive code and the number of required changes. Sadly, many C++ developers are not aware of how useful this keyword is. Especially embedded code can gain a lot by its usage.
In this article, I try to explain a number of useful cases of the auto
keyword, using Arduino example code. The same principles are valid for any embedded environment which is using a modern C++ compiler. The C++11 standard is eight years old, and meanwhile, every C++ compiler should support it.
The examples are written for Arduino or Adafruit SAMD boards using the Arduino IDE. The toolchain for these boards use a compiler which supports more features explained in this article. Using auto
with AVR code is possible too, just give it a try.
The “auto” Keyword
The auto
keyword was introduced to instruct the compiler to automatically deduce a type from the given context. In programming language terms, they call this a placeholder type specifier.
C++11 mainly introduced the auto
keyword for variable declarations. These are the main focus of this article.
Simple Variable Declarations
At any point in your code, where you declare a variable which is initialized, you can use auto
instead of a type.
auto variable = <some expression>
The following example uses auto
in various places:
const auto cFrameSize = 100; auto gMiddleValue = 0; void setup() { pinMode(LED_BUILTIN, OUTPUT); } void waitUntil(uint32_t timePoint) { while (static_cast<int32_t>(timePoint - millis()) > 0) {} } void loop() { const auto currentTime = millis(); const auto middlePoint = currentTime + (cFrameSize - gMiddleValue); const auto framePoint = currentTime + cFrameSize; digitalWrite(LED_BUILTIN, HIGH); waitUntil(middlePoint); digitalWrite(LED_BUILTIN, LOW); waitUntil(framePoint); gMiddleValue += 1; if (gMiddleValue >= cFrameSize) { gMiddleValue = 0; } }
Variables from Literal Values
The example starts with two variable declarations which are initialized from integer literals:
const auto cFrameSize = 100; auto gMiddleValue = 0;
The first line is also adding const
to create a constant while the second line declares a regular global variable.
Using auto
with integer values as shown is not very useful and just used as a demonstration. Especially most people will now know the actual type generated (int). It starts to make sense if you are working with a class
, a struct
and especially template classes.
If you read my previous article, there I declare a new template class Duration
and user-defined literals for time values. To use a millisecond constant, I would have to write:
#include "Duration.hpp" const Millisecond cSerialTimeout = 100_ms;
If I change the value from milliseconds to microseconds, I also have to change the variable type:
#include "Duration.hpp" const Microseconds cSerialTimeout = 100_us;
If I use auto
as a variable type, no change is needed. I especially do not need to remember the correct type for _ms
or _us
literals:
#include "Duration.hpp" const auto cSerialTimeout = 100_us;
Variables for Immediate Results
In the example, I also use auto
to create variables for immediate calculation results:
const auto currentTime = millis(); const auto middlePoint = currentTime + (cFrameSize - gMiddleValue); const auto framePoint = currentTime + cFrameSize;
The const
keyword is added to create a constant variable instead of a regular one.
Using auto
for immediate variables makes the code shorter and more flexible. If the real-time counter type would change from uint32_t
to uint16_t
, you do not have to change this code at all.
Simplify Range Based For Loops
The auto
keyword is great in range based for
loops:
const char* magicLines[] = { "Twinkle, twinkle, little star,", "How I wonder what you are!", "Up above the world so high,", "Like a diamond in the sky.", "When this blazing sun is gone,", "When he nothing shines upon,", "Then you show your little light,", "Twinkle, twinkle, through the night.", }; void setup() { Serial.begin(115200); while (!Serial) {}; for (auto line : magicLines) { Serial.println(line); } } void loop() { }
If you are using complex types, it makes sense to use a const
reference in the for
loop as shown in the next example:
struct MagicLine { const char *text; uint32_t delay; }; const MagicLine magicLines[] = { {"Twinkle, twinkle, little star,", 800}, {"How I wonder what you are!", 1000}, {"Up above the world so high,", 1200}, {"Like a diamond in the sky.", 2000}, {"When this blazing sun is gone,", 500}, {"When he nothing shines upon,", 800}, {"Then you show your little light,", 1000}, {"Twinkle, twinkle, through the night.", 500}, }; void setup() { Serial.begin(115200); while (!Serial) {}; for (const auto &line : magicLines) { Serial.println(line.text); delay(line.delay); } } void loop() { }
Make Register Access Configurable
In embedded software, you often have to deal with the low-level hardware API for performance reasons. This usually includes direct register access.
The hardware API of the manufacturer of most microcontrollers is an unfathomable mess of macros and nested structures. It can take a very long time to find the correct type names in these files.
Instead of doing all this research, you can simply use an auto
reference to the registers or structures. You can give this reference a short, easy to read name. As shown in the following example:
// For Adafruit Feather M0 constexpr auto &port = PORT->Group[0]; constexpr auto &portDir = port.DIR.reg; constexpr auto &portOut = port.OUTTGL.reg; const auto mask = 0b100000000000000000u; void setup() { portDir |= mask; } void loop() { portOut = mask; delay(200); }
The variables port
, portDir
and portOut
create a simple reference to the registers and/or structures which can be easily used in the following code. Changing the port can be quickly done in the first line, without touching the rest of the code.
I use constexpr
in front of these variables to make sure the compiler is using the original location of the reference and does not implement the variable as a pointer to these registers.
Inline Functions made Simple
Another interesting use of the auto
keyword is to declare a variable for an labda function quickly. In some cases, you need a very small function which only makes sense in the context of another function. Many languages support inline functions for this situation.
In C++ you can use lambda functions in this situation, they are declared as shown in the following example:
void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { auto flashLed = [](uint32_t duration) { digitalWrite(LED_BUILTIN, HIGH); delay(duration); digitalWrite(LED_BUILTIN, LOW); delay(100); }; flashLed(100); flashLed(200); flashLed(300); }
The example above could be easily written as shown in the next example:
void setup() { pinMode(LED_BUILTIN, OUTPUT); } void flashLed(uint32_t duration) { digitalWrite(LED_BUILTIN, HIGH); delay(duration); digitalWrite(LED_BUILTIN, LOW); delay(100); } void loop() { flashLed(100); flashLed(200); flashLed(300); }
Inline functions get interesting if you access local function variables you otherwise had to pass as parameters. This is the case in the following abstract example:
void addStep(int step, int maximum, int &counterA, int &counterB, int &counterC) { counterA += step; counterB += step; counterC += step; if (counterA > maximum) { counterA = maximum; } } void increaser(int maximum) { int counterA = 0; int counterB = 2; int counterC = 10; addStep(6, maximum, counterA, counterB, counterC); addStep(20, maximum, counterA, counterB, counterC); addStep(12, maximum, counterA, counterB, counterC); addStep(20, maximum, counterA, counterB, counterC); }
The example above can be written like this:
void increaser(int maximum) { int counterA = 0; int counterB = 2; int counterC = 10; auto addStep = [&](int step) { counterA += step; counterB += step; counterC += step; if (counterA > maximum) { counterA = maximum; } }; addStep(6); addStep(20); addStep(12); addStep(20); }
The second implementation is shorter with less repetitition. It also removes the function addStep
from the global namespace, which is only used by the increaser
function.
Learn More
- Real Time Counter and Integer Overflow
- Guide to Modular Firmware
- Class or Module for Singletons?
- Event-based Firmware
- It’s Time to Use #pragma once
- How and Why to use Namespaces
Conclusion
The auto
keyword can make your code shorter and reduces repetitive parts. If used with care, it improves the overall code quality and even simplifies refactoring. Give it a try!
If you have questions, miss some information or have any feedback, feel free to add a comment below.
Learn More

How and Why to use Namespaces

Units of Measurements for Safe C++ Variables

Consistent Error Handling

Real Time Counter and Integer Overflow

Bit Manipulation using Templates

Thanks for writing those articles! For someone who didn’t touch C++ for 10+ years and spent all that time with Python this info brings the sanity back!
Thank you! I am glad you liked the articles and have a look at all the nice new features in C++17 and C++20. 🙂