C++ Dangling References: Lifetime, Temporaries, and Containers

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

  • 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


  • Buffer overflow
  • Lifetime
  • optional
  • Reference collapsing
  • References guide (series)