C++ Dangling References: Lifetime, Temporaries, and Containers
이 글의 핵심
Practical guide to dangling references: why returning references to locals is UB, temporary lifetime rules, and how to fix and detect issues.
What is a dangling reference?
A reference to storage whose lifetime has ended.
// Dangling reference
const std::string& func() {
std::string s = "Hello";
return s; // s destroyed when function returns
}
int main() {
const std::string& ref = func();
// ref refers to destroyed object (UB)
}
Common causes
// 1. Return reference to local
const int& func1() {
int x = 10;
return x; // x is gone
}
// 2. Temporary
const char* func2() {
return std::string("Hello").c_str(); // temporary destroyed
}
// 3. Container element
const int& func3() {
std::vector<int> vec = {1, 2, 3};
return vec[0]; // vec destroyed
}
// 4. Dereference after delete
int& func4() {
int* ptr = new int(10);
delete ptr;
return *ptr; // freed memory
}
Practical examples
Example 1: Function returns
#include <string>
// Bad: reference to local
const std::string& getName() {
std::string name = "Alice";
return name; // UB
}
// Good: return by value
std::string getName() {
std::string name = "Alice";
return name; // safe (copy or move)
}
// Good: static storage
const std::string& getStaticName() {
static std::string name = "Alice";
return name; // safe
}
int main() {
auto name1 = getName(); // safe
const auto& name2 = getStaticName(); // safe
}
Example 2: Temporaries
#include <vector>
std::vector<int> getVector() {
return {1, 2, 3};
}
int main() {
// Dangerous: temporary from getVector() dies
const int& first = getVector()[0];
// Safe: keep container alive
auto vec = getVector();
const int& first = vec[0]; // safe
// Safe: lifetime extension via const ref to temporary
const auto& vec2 = getVector();
const int& first2 = vec2[0]; // safe
}
Example 3: map::operator[]
#include <map>
#include <string>
class Database {
private:
std::map<int, std::string> data;
public:
// Risky: may insert default-constructed temporary
const std::string& getValue(int key) {
return data[key];
}
// Better: return by value or optional
std::string getValue(int key) {
auto it = data.find(key);
return it != data.end() ? it->second : "";
}
const std::string* getValuePtr(int key) {
auto it = data.find(key);
return it != data.end() ? &it->second : nullptr;
}
};
Example 4: Member accessors
class Widget {
private:
std::string name;
public:
Widget(const std::string& n) : name(n) {}
// OK: member reference
const std::string& getName() const {
return name;
}
// Bad: reference to temporary
const std::string& getUpperName() const {
return toUpper(name); // temporary
}
// Good: return by value
std::string getUpperName() const {
return toUpper(name);
}
private:
std::string toUpper(const std::string& s) const {
std::string result = s;
// upper-case transform
return result;
}
};
Common pitfalls
Pitfall 1: Iterator/reference invalidation
#include <vector>
std::vector<int> vec = {1, 2, 3};
auto& ref = vec[0];
vec.push_back(4); // may reallocate
// ref may dangle
vec.push_back(4);
auto& ref = vec[0]; // re-bind after growth
Pitfall 2: Smart pointers
#include <memory>
class Data {
public:
int value = 42;
};
std::unique_ptr<Data> getData() {
return std::make_unique<Data>();
}
int main() {
auto ptr = getData();
int& ref = ptr->value;
ptr.reset(); // freed
// ref dangles
}
// Safer: copy value
int main() {
auto ptr = std::make_shared<Data>();
int value = ptr->value;
ptr.reset();
// value still valid
}
Pitfall 3: Lambda capture
#include <functional>
// Bad: reference capture of local
std::function<int()> createGetter() {
int x = 10;
return [&x]() { return x; }; // x destroyed
}
int main() {
auto getter = createGetter();
int value = getter(); // UB
}
// Good: copy capture
std::function<int()> createGetter() {
int x = 10;
return [x]() { return x; }; // safe
}
Pitfall 4: Chaining on temporaries
class Builder {
private:
std::string data;
public:
Builder& append(const std::string& s) {
data += s;
return *this;
}
};
Builder createBuilder() {
return Builder();
}
int main() {
// Bad: temporary destroyed after statement
auto& builder = createBuilder().append("Hello");
// Good: value
auto builder = createBuilder().append("Hello");
}
Detection
// Compiler warnings
// -Wreturn-local-addr (GCC)
// -Wreturn-stack-address (Clang)
// Static analysis
// Clang Static Analyzer, Cppcheck, PVS-Studio
// Runtime
// AddressSanitizer, Valgrind
Fixes
// 1. Return by value
std::string func() {
std::string s = "Hello";
return s;
}
// 2. Smart pointers
std::shared_ptr<Data> func() {
return std::make_shared<Data>();
}
// 3. Out parameter
void func(std::string& out) {
out = "Hello";
}
// 4. Static
const std::string& func() {
static std::string s = "Hello";
return s;
}
// 5. Member reference
class Widget {
std::string name;
public:
const std::string& getName() const {
return name; // safe
}
};
FAQ
Q1: When do references dangle?
A:
- Return reference to local
- Hold reference/pointer into dead temporary
- Use after free
Q2: How to detect?
A:
- Compiler warnings
- Static analyzers
- AddressSanitizer
Q3: Fixes?
A:
- Return by value
- Smart pointers
- Extend lifetime explicitly
Q4: Performance?
A: RVO/move often makes return-by-value cheap.
Q5: “Safe” references?
A:
- To members of longer-lived objects
- To statics
- Parameters in scope
Q6: Learning resources?
A:
- Effective C++
- C++ Core Guidelines
- cppreference.com
Related posts (internal links)
- Use after free
- Buffer overflow
- Lifetime
Practical tips
Debugging
- Warnings first, minimal repro
Performance
- Profile before tuning
Code review
- Team conventions
Practical checklist
Before coding
- Right approach?
- Maintainable?
- Performance OK?
While coding
- Warnings clean?
- Edge cases?
- Error handling?
At review
- Intent clear?
- Tests?
- Docs?
Keywords
C++, dangling reference, lifetime, reference, undefined behavior
Related posts
- Buffer overflow
- Lifetime
- optional
- Reference collapsing
- References guide (series)