Avoiding Bugs in Range-Based For-Loops with MISRA C++:2023®
MISRA C++:2023®, the next version of the MISRA C++ standard, is here! To help you understand the changes between MISRA C++:2023 and the previous version, we continue our blog series by Perforce Principal Technical Support Engineer, Dr. Frank van den Beuken, with this third installment.
In the first two blogs, we introduced you to the new MISRA C++ standard and the history of C++. In this blog, we take a closer look at a specific rule centering on for-loops in C++.
Request Your Free Trial Helix QAC
Follow along or jump ahead to the section that interests you most.
What Is MISRA C++:2023 Rule 9.5.2 and Why Is It Important?
MISRA C++:2023 introduces Rule 9.5.2, "A for-range initializer shall contain at most one function call" to avoid the undefined behavior that can occur when the for-range initializer of a range-based for-statement creates a temporary object.
To understand why this can happen, let's take a closer look at the C++ range-based for-loop.
Back to topWhat Is a Range-Based For-Loop in C++?
In programming, loops are used to repeat a block of code. When you know how many times you want to loop through a block of code, use for
loop.
The C++ range-based for-loop was introduced in C++11 as a concise notation for iteration over a container.
The traditional for
loop originates from the C language and has an optional loop initialization, followed by the loop condition and finally the loop increment expression.
Traditional for
loops can be used to iterate over a container, as shown below:
std::vector v = { "Example", "vector", "of", "strings" };
for ( auto &&i = v.begin(); i != v.end(); ++i ) {
std::cout << *i << “ “;
}
std::cout << std::endl;
Using range-based for
, the use of the iterator is left implicit:
for ( auto &&s: v ) {
std::cout << s << “ “;
}
This is a much simpler notation for the same loop. The C++ language standard states that it is short for:
{
auto && __range = v;
auto __begin = __range;
auto __end = v.end();
for (; __begin != __end; ++__begin) {
auto &&s = *__begin;
std::cout << s << “ “;
}
}
However, there is a limitation with this notation. In the above example, __range is initialized with v, which is a simpler variable, but it is also possible to use a complex expression for which more than one temporary object is created.
Let us consider using a function that returns the vector of strings and have:
- One loop that outputs the strings separated by spaces, as above
- A second loop that prints the letters of the first string separated by spaces:
std::vector<std::string> createStrings() {
return { "Example”, "vector", "of", "strings" };
}
int main() {
for ( auto w: createStrings() ) { std::cout << w << " "; }
std::cout << std::endl;
for ( auto c: createStrings()[0] ) { std::cout << c << " "; }
std::cout << std::endl;
}
If we execute this, the first loop will behave as expected, but the second loop invokes undefined behavior. The problem is that createStrings()[0] has two function calls. The innermost call is the call of createStrings and the outermost call is to the index operator[].
The reason for the undefined behavior is that the temporary object returned by ‘createStrings’ is used as an argument to the ‘operator[]’ call, and therefore, according to the rules of C++, the temporary does not have its lifetime extended.
Back to topHow MISRA C++:2023 Rule 9.5.2 Guards Against Undefined Behavior
MISRA C++:2023 Rule 9.5.2 has been designed to guard against this. MISRA C++:2023 introduced Rule 9.5.2, which requires that the for-range-initializer shall contain at most one function call.
It also suggests solving this problem by performing the inner function calls in a separate declaration before the range-for loop. For example:
auto strings = createStrings();
for ( auto c: strings[0] ) { std::cout << c << " "; }
Now there is only one function call in the initializer so that lifetime extension has the desired effect, and the behavior is fully defined.
Note that this issue has been addressed in C++23, where the lifetime of all temporaries of the initializer are extended to the entire for-statement.
📕 Related Resource: A Guide to MISRA C
Back to topEnforce MISRA C++:2023 Rules with Helix QAC
Perforce’s Helix QAC is a static analysis tool at the forefront of delivering MISRA C and MISRA C++ compliance checking as well as a host of other valuable analysis capabilities.
Helix QAC provides 100% enforcement coverage for MISRA C++:2023 rules with its compliance module for the standard, available now. The static analysis tool finds and reports on violations of MISRA rules and directives in C and C++.
See why Helix QAC is the best static code analyzer for MISRA C++.
➡️ Sign Up for a Free 7-Day Trial
Back to top