The address of an automatic object should not be persisted beyond the object’s lifetime.
Why is this an issue?
An automatic object is an object whose lifetime is automatically managed. The storage for an automatic object, e.g. a local variable, is allocated
at the beginning of the enclosing code block and is deallocated at the end. This is commonly referred to as "allocated on the stack".
If the address of an automatic object is assigned to another automatic object of larger scope, a static or extern object, or if it is returned from
a function (using return
or an output parameter), then there will be a point where the address will point to an object that ceased to
exist. In that case, the address becomes invalid, and attempts to dereference the invalid address — trying to access the object that ceased to
exist — result in undefined behavior.
int *global = nullptr;
int* bar(int **out) {
int local = 42;
int *ptr;
global = &local; // Noncompliant: assigning the address of an object allocated on the stack to a global variable
{
int i = 9001;
ptr = &i; // Noncompliant: assigning the address of a stack-allocated object to an object that outlives it
}
*out = &local; // Noncompliant: returning the address of an object allocated on the stack (via output parameter)
return &local; // Noncompliant: returning the address of an object allocated on the stack
}
What is the potential impact?
Persisting addresses of objects with automatic storage past their lifetime is dangerous as it creates invalid addresses. Attempts to access objects
that no longer exist through such invalid addresses result in undefined behavior.
For programs that exercise undefined behavior, the compiler is no longer bound by the language specification. The application may crash or, even
worse, the application may appear to execute correctly while losing data or producing incorrect results.
How to fix it
There are multiple approaches to avoid persisting addresses of objects with automatic storage after their lifetime ended:
- Make a full copy of the object instead of using its address.
- Allocate the object on the heap (free-store) and deallocate it once done.
- Create the automatic object in a larger scope where it outlives all accesses through its address.
- Reduce the scope of objects that store the address of the original automatic object.
Code examples
Noncompliant code example
int* bar(void) {
int local_auto = 42;
return &local_auto; // Noncompliant: returns the address of a stack-allocated object
}
Compliant solution
int foo() {
int local_auto = 42;
return local_auto; // Compliant: returns a value rather than an address
}
int* foo() {
int *local_auto = new int(42);
return local_auto; // Compliant: returns the address of a heap-allocated object
}
Noncompliant code example
const char *str_ptr = nullptr;
void bar() {
const char str[] = "This will change";
str_ptr = str; // Noncompliant: address of `str` becomes invalid when `str` goes out of scope
}
Compliant solution
void bar() {
const char str[] = "This will change";
const char *str_ptr = str; // Compliant: `str_ptr` has the same storage duration as `str`
}
#include <string>
std::string bar() {
std::string str = "This will change";
return str; // Compliant: `str` will be moved to the caller (using C++ move semantics)
}
Noncompliant code example
#include <array>
#include <cstddef>
int *buz() {
int buf[256];
for (size_t i = 0; i < std::size(buf); ++i) {
buf[i] = i;
}
return buf; // Noncompliant: returns address of stack-allocated object
}
Compliant solution
#include <array>
#include <cstddef>
#include <numeric>
void buz(int *buf, size_t length) {
std::iota(&buf[0], &buf[length], 0);
}
void caller() {
int buf[256];
buz(buf, std::size(buf));
}
#include <array>
std::array<int, 3> buz() {
std::array<int, 3> buf = {1, 2, 3};
return buf; // Compliant: `buf` will by copied to the caller; return-value optimization (RVO) might be invoked
}
#include <numeric>
#include <vector>
std::vector<int> buz() {
std::vector<int> buf;
std::iota(buf.begin(), buf.end(), 0);
return buf; // Compliant: `buf` will be moved to the caller (using C++ move semantics)
}
Noncompliant code example
#include <algorithm>
#include <array>
void fun(char **out) {
char buffer[64];
std::fill(std::begin(buffer), std::end(buffer), 42);
*out = buffer; // Noncompliant: `buffer`'s address becomes invalid once it goes out of scope
}
void caller() {
char *p;
fun(&p);
}
Compliant solution
#include <algorithm>
#include <array>
char buffer[64];
void fun(char **out) {
std::fill(std::begin(buffer), std::end(buffer), 42);
*out = buffer; // Compliant: `buffer`'s lifetime is the program's lifetime
}
void caller() {
char *p;
fun(&p);
}
#include <vector>
std::vector<int> fun() {
std::vector<int> buf(/* count */ 64, /* initial value */ 42);
return buf;
}
void caller() {
auto buf = fun();
}
Resources
Conference presentations
Standards
Related rules
- S837 detects attempts to return addresses of automatic variables
- S839 ensures that functions do not return references or pointers to parameters that are passed by reference