C++ RAII & Smart Pointers: Ownership and Automatic Cleanup

C++ RAII & Smart Pointers: Ownership and Automatic Cleanup

이 글의 핵심

Practical guide to RAII and smart pointers—how they work together for safe resource and memory management.

What are RAII and smart pointers?

RAII (Resource Acquisition Is Initialization) means binding resources to object lifetime: acquire in construction, release in destruction. Smart pointers are the standard way to express heap ownership with RAII.

// ❌ Manual management
int* ptr = new int(10);
// ... leak when exception occurs
delete ptr;

// ✅ Smart pointer
auto ptr = std::make_unique<int>(10);
// automatic destruction

Why do you need it?:

  • Auto-Release: Automatic cleanup in destructor.
  • Exception safety: Safe even when exceptions occur
  • Memory leak prevention: Prevent missing deletes
  • Clear ownership: It is clear who owns the resource.
// ❌ Manual management: Leakage on exception
void func() {
    int* ptr = new int(10);
    
    if (error) {
throw std::runtime_error("error");// Leak!
    }
    
    delete ptr;
}

// ✅ RAII: exception safe
void func() {
    auto ptr = std::make_unique<int>(10);
    
    if (error) {
throw std::runtime_error("error");// automatic release
    }
}

RAII operating principle:

flowchart LR
    A[Construct object] --> B[Acquire resource]
    B --> C[Use]
    C --> D[Exception?]
    D -->|Yes| E[Stack unwinds]
    D -->|No| F[Normal exit]
    E --> G[Destructor runs]
    F --> G
    G --> H[Release resource]

Smart pointer types:

TypeOwnershipCopyGoUsage Scenarios
unique_ptrExclusiveSingle Owner
shared_ptrShareMultiple Owners
weak_ptrNoneAvoid circular references
// unique_ptr: exclusive ownership
std::unique_ptr<int> u = std::make_unique<int>(10);
// auto u2 = u;// Error: Copy not possible
auto u2 = std::move(u);// OK: Move

// shared_ptr: shared ownership
std::shared_ptr<int> s = std::make_shared<int>(10);
auto s2 = s;// OK: copy (increment reference count)

// weak_ptr: observation only
std::weak_ptr<int> w = s;
if (auto ptr = w.lock()) { // lock when used
    std::cout << *ptr << '\n';
}

unique_ptr

#include <memory>

// exclusive ownership
std::unique_ptr<int> ptr = std::make_unique<int>(10);

// Only movement possible
auto ptr2 = std::move(ptr);  // ptr is nullptr

shared_ptr

// shared ownership
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1;// increase reference count

// Resources are released when the last shared_ptr is destroyed

Practical example

Example 1: unique_ptr default

class Resource {
public:
    Resource() { std::cout << "ctor" << std::endl; }
    ~Resource() { std::cout << "dtor" << std::endl; }
    
    void use() { std::cout << "use" << std::endl; }
};

void func() {
    auto ptr = std::make_unique<Resource>();
    ptr->use();
// Automatically destroyed when the function ends
}

int main() {
    func();
// "Create" -> "Use" -> "Destroy"
}

Example 2: shared_ptr reference count

void func() {
    auto ptr1 = std::make_shared<int>(10);
std::cout << "Count: " << ptr1.use_count() << std::endl;// 1
    
    {
        auto ptr2 = ptr1;
std::cout << "Count: " << ptr1.use_count() << std::endl;// 2
    }
    
std::cout << "Count: " << ptr1.use_count() << std::endl;// 1
}

Example 3: Container

std::vector<std::unique_ptr<Widget>> widgets;

widgets.push_back(std::make_unique<Widget>(1));
widgets.push_back(std::make_unique<Widget>(2));

// When the vector is destroyed, all widgets are automatically destroyed.

Example 4: Factory pattern

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Circle" << std::endl;
    }
};

std::unique_ptr<Shape> createShape(const std::string& type) {
    if (type == "circle") {
        return std::make_unique<Circle>();
    }
    return nullptr;
}

weak_ptr

auto shared = std::make_shared<int>(10);
std::weak_ptr<int> weak = shared;

// lock when using
if (auto ptr = weak.lock()) {
    std::cout << *ptr << std::endl;
}

Frequently occurring problems

Problem 1: Circular references

// ❌ Circular reference
class Node {
public:
    std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;// circular reference
};

// ✅ Use weak_ptr
class Node {
public:
    std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;// prevent circulation
};

Problem 2: this pointer

// ❌ this to shared_ptr
class Bad {
public:
    std::shared_ptr<Bad> getPtr() {
return std::shared_ptr<Bad>(this);  // unsafe!
    }
};

// ✅ enable_shared_from_this
class Good : public std::enable_shared_from_this<Good> {
public:
    std::shared_ptr<Good> getPtr() {
        return shared_from_this();
    }
};

Problem 3: Arrays

// ❌ Array deletion problem
std::unique_ptr<int> ptr(new int[10]);  // calls delete (wrong for array)

// ✅ Array specialization
std::unique_ptr<int[]> ptr(new int[10]);  // calls delete[]

// ✅ make_unique (C++14)
auto ptr = std::make_unique<int[]>(10);

Issue 4: Custom deleter

// FILE* management
auto deleter = [](FILE* f) {
    if (f) fclose(f);
};

std::unique_ptr<FILE, decltype(deleter)> file(
    fopen("file.txt", "r"), deleter
);

Performance comparison

// unique_ptr: pointer-sized
sizeof(std::unique_ptr<int>);  // typically 8 bytes

// shared_ptr: object ptr + control block ptr
sizeof(std::shared_ptr<int>);  // typically 16 bytes

Production patterns

Pattern 1: Factory function

class Connection {
public:
    Connection(const std::string& host) : host_(host) {
        std::cout << "connect: " << host_ << '\n';
    }
    
    ~Connection() {
        std::cout << "disconnect: " << host_ << '\n';
    }
    
    void query(const std::string& sql) {
        std::cout << "query: " << sql << '\n';
    }
    
private:
    std::string host_;
};

std::unique_ptr<Connection> createConnection(const std::string& host) {
    return std::make_unique<Connection>(host);
}

// use
auto conn = createConnection("localhost");
conn->query("SELECT * FROM users");
// Automatic connection termination

Pattern 2: Cache System

#include <map>
#include <memory>
#include <string>

class Cache {
    std::map<std::string, std::weak_ptr<Resource>> cache_;
    
public:
    std::shared_ptr<Resource> get(const std::string& key) {
// check cache
        if (auto it = cache_.find(key); it != cache_.end()) {
            if (auto ptr = it->second.lock()) {
return ptr;// cache hit
            }
        }
        
// Cache miss: create new
        auto resource = std::make_shared<Resource>(key);
        cache_[key] = resource;
        return resource;
    }
};

Pattern 3: Passing ownership

class TaskQueue {
    std::vector<std::unique_ptr<Task>> tasks_;
    
public:
    void addTask(std::unique_ptr<Task> task) {
        tasks_.push_back(std::move(task));
    }
    
    std::unique_ptr<Task> getNextTask() {
        if (tasks_.empty()) {
            return nullptr;
        }
        
        auto task = std::move(tasks_.back());
        tasks_.pop_back();
        return task;
    }
};

// use
TaskQueue queue;
queue.addTask(std::make_unique<Task>("Task1"));

auto task = queue.getNextTask();// transfer ownership
task->execute();

FAQ

Q1: When should I use it?

A:

  • unique_ptr: exclusive ownership, single owner
  • shared_ptr: shared ownership, multiple owners
  • weak_ptr: Avoid circular references, observe only.
// unique_ptr: exclusive
std::unique_ptr<Widget> widget = std::make_unique<Widget>();

// shared_ptr: shared
std::shared_ptr<Widget> shared = std::make_shared<Widget>();
auto shared2 = shared;// copy

// weak_ptr: observation
std::weak_ptr<Widget> weak = shared;

Q2: What is the performance?

A:

  • unique_ptr: Same as raw pointer (no overhead)
  • shared_ptr: Reference count overhead (atomic operation)
// unique_ptr: 8 bytes
sizeof(std::unique_ptr<int>);  // 8

// shared_ptr: 16 bytes (pointer + control block)
sizeof(std::shared_ptr<int>);  // 16

Q3: How do I use arrays?

A: Use unique_ptr<T[]>.

// ❌ Array deletion problem
std::unique_ptr<int> ptr(new int[10]);// call delete (wrong)

// ✅ Array specialization
std::unique_ptr<int[]> ptr(new int[10]);  // calls delete[]

// ✅ make_unique (C++14)
auto ptr = std::make_unique<int[]>(10);

Q4: How do I resolve circular references?

A: Solved with weak_ptr.

// ❌ Circular reference
class Node {
    std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;// circular reference
};

// ✅ weak_ptr
class Node {
    std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;// prevent circulation
};

Q5: When to use raw pointer?

A:

  • No Ownership: Observation only
  • Function parameters: temporary reference
  • Performance Critical: Hot Pass
// raw pointer: no ownership
void process(Widget* widget) {
widget->use();// observation only
}

// unique_ptr: Ownership
std::unique_ptr<Widget> owner = std::make_unique<Widget>();
process(owner.get());  // pass raw pointer (non-owning)

Q6: make_unique vs new?

A: make_unique is recommended.

// ❌ new: exception unsafe
func(std::unique_ptr<Widget>(new Widget()), compute());
// Possible leak when exception occurs in compute()

// ✅ make_unique: exception safe
func(std::make_unique<Widget>(), compute());

Q7: What about custom deleters?

A: Use lambda or function object.

// FILE* management
auto deleter = [](FILE* f) {
    if (f) fclose(f);
};

std::unique_ptr<FILE, decltype(deleter)> file(
    fopen("file.txt", "r"), deleter
);

Q8: What are smart pointer learning resources?

A:

See also: unique_ptr, shared_ptr, weak_ptr.

One-line summary: RAII and smart pointers are core C++ techniques that prevent memory leaks with automatic resource management.


Good article to read together (internal link)

Here’s another article related to this topic.

  • C++ smart pointer |unique_ptr/shared_ptr “Memory Safety” Guide
  • C++ smart pointer |A solution to a circular reference bug that I couldn’t find in 3 days
  • Complete guide to C++ smart pointer basics |unique_ptr·shared_ptr

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intention of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, RAII, smart-pointers, unique_ptr, shared_ptr, etc.


  • C++ shared_ptr vs unique_ptr |
  • C++ smart pointer |A solution to a circular reference bug that I couldn’t find in 3 days
  • Complete guide to C++ smart pointer basics |unique_ptr·shared_ptr
  • C++ RAII Complete Guide |
  • C++ smart pointer |unique_ptr/shared_ptr