Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Accessing Raw Pointers from std::unique_ptr<T>

As a programmer working with std::unique_ptr<T>, there are instances where you need access to the underlying raw pointer managed by the unique_ptr. Perhaps you need to pass it to a legacy C API that expects raw pointers, or you need to share the pointer temporarily with another part of your code while maintaining the unique ownership model.

To support this need, std::unique_ptr provides two distinct methods for retrieving the raw pointer:

  1. get() - Get the pointer without changing ownership
  2. release() - Get the pointer AND transfer ownership

The crucial difference between these two methods lies in what happens to the unique_ptr’s ownership after calling them. Understanding this distinction is vital for writing safe and correct C++ code.


The get() Method

What Does get() Do?

The get() method returns a raw pointer to the underlying resource without transferring ownership. After calling get(), the unique_ptr still holds full responsibility for managing and cleaning up the resource. When you call get(), you’re essentially saying: “I need temporary access to this pointer, but you (unique_ptr) keep managing it.”

Key Characteristics

T* get() const noexcept;
  • Returns: A raw pointer (T*) to the managed object
  • Returns nullptr if the unique_ptr is empty
  • Ownership: Remains with the unique_ptr
  • Responsibility for cleanup: The unique_ptr still owns and will clean up the resource
  • When cleanup happens: When the unique_ptr goes out of scope or is reassigned

Basic Example: Temporary Pointer Access

#include <memory>
#include <iostream>
#include <cstring>

class FileBuffer {
public:
    FileBuffer(size_t size) : size_(size) {
        std::cout << "FileBuffer allocated (" << size_ << " bytes)\n";
    }
    ~FileBuffer() {
        std::cout << "FileBuffer deallocated\n";
    }
private:
    size_t size_;
};

int main() {
    // Create a unique_ptr managing a FileBuffer
    std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBuffer>(1024);
    
    std::cout << "\n--- Using get() ---\n";
    // get() returns the raw pointer without transferring ownership
    FileBuffer* ptr = buffer.get();
    
    std::cout << "Obtained raw pointer: " << ptr << "\n";
    std::cout << "unique_ptr still owns the buffer\n";
    
    // Use the pointer temporarily
    std::cout << "Using the pointer for operations...\n";
    
    // Do NOT delete ptr here!
    // The unique_ptr will handle cleanup
    
    std::cout << "\nExiting scope...\n";
    // When buffer goes out of scope, the FileBuffer is automatically destroyed
    
    return 0;
}

// Output:
// FileBuffer allocated (1024 bytes)
// 
// --- Using get() ---
// Obtained raw pointer: 0x556a8620
// unique_ptr still owns the buffer
// Using the pointer for operations...
// 
// Exiting scope...
// FileBuffer deallocated

Why get() Is Dangerous

The get() method returns a non-owning pointer. This creates a critical danger: you must never use the pointer after the unique_ptr destroys the object it was managing.

Danger: Use-After-Free

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource created\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void doSomething() { std::cout << "Doing work...\n"; }
};

int main() {
    Resource* dangling_ptr = nullptr;
    
    {
        std::unique_ptr<Resource> res = std::make_unique<Resource>();
        
        // Get raw pointer
        dangling_ptr = res.get();
        
        std::cout << "Pointer obtained\n";
        // res goes out of scope here - Resource is destroyed
    }
    
    std::cout << "Outside scope\n";
    
    // DANGER: dangling_ptr now points to freed memory!
    // dangling_ptr->doSomething();  // UNDEFINED BEHAVIOR - DO NOT DO THIS!
    
    return 0;
}

// Output:
// Resource created
// Pointer obtained
// Resource destroyed
// Outside scope

The raw pointer from get() is only valid as long as the unique_ptr manages the resource. Once the unique_ptr goes out of scope or is reassigned, the pointer becomes dangling and must never be accessed.


Using get() with Legacy C APIs

One of the legitimate uses of get() is passing the pointer to legacy C-style functions that expect raw pointers but don’t take ownership:

#include <memory>
#include <cstdio>

// Legacy C function - doesn't own the pointer
void legacyPrintData(const char* data) {
    std::printf("Data: %s\n", data);
}

int main() {
    std::unique_ptr<char[]> buffer = std::make_unique<char[]>(100);
    
    // Fill the buffer
    std::strcpy(buffer.get(), "Hello, World!");
    
    // Pass to legacy function using get()
    legacyPrintData(buffer.get());
    
    // buffer still owns the memory
    // Cleanup happens automatically when buffer goes out of scope
    
    return 0;
}

get() with Custom Deleters and Non-Pointer Resources

When using unique_ptr with custom deleters to manage resources other than heap memory (like file descriptors, database connections, etc.), get() returns a pointer to the underlying resource representation.

Example: File Descriptor

#include <memory>
#include <unistd.h>
#include <fcntl.h>
#include <iostream>

struct FileDescriptorDeleter {
    void operator()(int* fd) const {
        if (fd && *fd >= 0) {
            std::cout << "Closing file descriptor " << *fd << "\n";
            close(*fd);
            delete fd;
        }
    }
};

int main() {
    int raw_fd = open("data.txt", O_RDONLY);
    
    std::unique_ptr<int, FileDescriptorDeleter> managed_fd(
        new int(raw_fd),
        FileDescriptorDeleter{}
    );
    
    std::cout << "Managing file descriptor\n";
    
    // get() returns pointer to the integer file descriptor
    int* fd_ptr = managed_fd.get();
    
    // Read using the file descriptor
    char buffer[100];
    if (read(*fd_ptr, buffer, sizeof(buffer)) > 0) {
        std::cout << "Successfully read from file\n";
    }
    
    // managed_fd still manages the resource and will close the fd
    return 0;
}

// Output:
// Managing file descriptor
// Successfully read from file
// Closing file descriptor 3

Example: Database Connection

#include <memory>
#include <iostream>

typedef int DB_HANDLE;

DB_HANDLE openDB(const std::string& name) {
    std::cout << "Connected to database: " << name << "\n";
    return 1001;  // Simulated handle
}

void closeDB(DB_HANDLE handle) {
    std::cout << "Disconnected from database (handle: " << handle << ")\n";
}

struct DBDeleter {
    void operator()(DB_HANDLE* handle) const {
        if (handle) {
            closeDB(*handle);
            delete handle;
        }
    }
};

int main() {
    std::unique_ptr<DB_HANDLE, DBDeleter> db(
        new DB_HANDLE(openDB("production")),
        DBDeleter{}
    );
    
    // get() returns pointer to the database handle
    DB_HANDLE* handle = db.get();
    
    std::cout << "Using database handle: " << *handle << "\n";
    std::cout << "Executing queries...\n";
    
    // db still manages the database connection
    // When it goes out of scope, the connection is closed
    return 0;
}

// Output:
// Connected to database: production
// Using database handle: 1001
// Executing queries...
// Disconnected from database (handle: 1001)

Best Practices for get()

  1. Use get() only for temporary access within a limited scope where the unique_ptr is still alive
  2. Never store the result of get() beyond the scope where the unique_ptr is valid
  3. Always check for nullptr before dereferencing:
    if (ptr.get() != nullptr) {
        // Safe to use
    }
    
  4. Prefer get() for read-only operations on legacy C APIs
  5. Never attempt to delete the pointer returned by get() - it’s not your responsibility
  6. Document that you’re using get() - make it clear you’re just borrowing the pointer

Summary: What get() Does

AspectBehavior
ReturnsRaw pointer to managed object
Ownership TransferNo - remains with unique_ptr
unique_ptr StateStill owns and manages the resource
Responsibilityunique_ptr cleans up on destruction
Safe to StoreOnly within scope where unique_ptr lives
Use CaseTemporary access, legacy C APIs


The release() Method

What Does release() Do?

The release() method returns the underlying raw pointer AND transfers ownership out of the unique_ptr. After calling release(), the unique_ptr becomes empty and is no longer responsible for managing the resource. The pointer’s recipient now owns it and must handle cleanup themselves.

When you call release(), you’re essentially saying: “I’m handing over full responsibility for this pointer. You now own it, and you must clean it up.”

Key Characteristics

T* release() noexcept;
  • Returns: A raw pointer (T*) to the previously managed object
  • Returns nullptr if the unique_ptr was already empty
  • Ownership: Transferred to the caller
  • unique_ptr state: Becomes empty/nullptr
  • Responsibility for cleanup: Caller must manage the returned pointer
  • Deleter applied: No - the deleter is NOT called by release()

Basic Example: Ownership Transfer

#include <memory>
#include <iostream>

class Data {
public:
    Data(int value) : value_(value) {
        std::cout << "Data(" << value_ << ") created\n";
    }
    ~Data() {
        std::cout << "Data(" << value_ << ") destroyed\n";
    }
private:
    int value_;
};

int main() {
    std::unique_ptr<Data> owned = std::make_unique<Data>(42);
    
    std::cout << "\n--- Using release() ---\n";
    
    // release() transfers ownership OUT of the unique_ptr
    Data* raw_ptr = owned.release();
    
    std::cout << "release() called\n";
    std::cout << "owned is now empty: " << (owned.get() == nullptr ? "true" : "false") << "\n";
    
    // Now raw_ptr owns the Data object
    // We are responsible for cleanup
    std::cout << "raw_ptr now owns the object\n";
    
    // Manual cleanup - WE must do this
    std::cout << "Manually deleting...\n";
    delete raw_ptr;
    
    return 0;
}

// Output:
// Data(42) created
// 
// --- Using release() ---
// release() called
// owned is now empty: true
// raw_ptr now owns the object
// Manually deleting...
// Data(42) destroyed

Critical Responsibility: Manual Cleanup

When you call release(), you are accepting full responsibility for cleaning up the resource. This is dangerous because:

  1. You must remember to delete it - forgetting causes memory leaks
  2. You must handle exceptions - if an exception occurs before deletion, you leak memory
  3. You cannot rely on automatic cleanup - the unique_ptr won’t help you

Danger: Memory Leak from Forgotten Deletion

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource allocated\n"; }
    ~Resource() { std::cout << "Resource deallocated\n"; }
};

int main() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    
    Resource* raw = res.release();
    
    // DANGER: If we forget to delete here, we have a MEMORY LEAK
    // The Resource is never cleaned up
    
    // CORRECT: Must remember to delete
    delete raw;
    
    return 0;
}

// Output:
// Resource allocated
// Resource deallocated

Legitimate Use Cases for release()

Example 1: Transferring to Another unique_ptr

#include <memory>
#include <iostream>

class Item {
public:
    Item(const std::string& name) : name_(name) {
        std::cout << "Item '" << name_ << "' created\n";
    }
    ~Item() {
        std::cout << "Item '" << name_ << "' destroyed\n";
    }
private:
    std::string name_;
};

int main() {
    std::unique_ptr<Item> ptr1 = std::make_unique<Item>("Sword");
    
    // Transfer ownership from ptr1 to ptr2
    std::unique_ptr<Item> ptr2(ptr1.release());
    
    // ptr1 is now empty
    std::cout << "ptr1 is empty: " << (ptr1.get() == nullptr ? "true" : "false") << "\n";
    
    // ptr2 now owns the Item and will clean it up automatically
    return 0;
}

// Output:
// Item 'Sword' created
// ptr1 is empty: true
// Item 'Sword' destroyed

Example 2: Returning from Legacy Interface

#include <memory>
#include <iostream>

class Buffer {
public:
    Buffer() { std::cout << "Buffer created\n"; }
    ~Buffer() { std::cout << "Buffer destroyed\n"; }
};

// Legacy C-style function that creates and returns a pointer
// Caller is responsible for deletion
Buffer* createBuffer() {
    auto buf = std::make_unique<Buffer>();
    return buf.release();  // Hand off ownership to caller
}

int main() {
    Buffer* buffer = createBuffer();
    
    std::cout << "Received buffer from legacy function\n";
    
    // Legacy code is responsible for cleanup
    delete buffer;
    
    return 0;
}

// Output:
// Buffer created
// Received buffer from legacy function
// Buffer destroyed

Example 3: Returning Raw Pointer with Custom Deleter

#include <memory>
#include <iostream>

typedef int DB_HANDLE;

DB_HANDLE openDB(const std::string& name) {
    std::cout << "Opening database: " << name << "\n";
    return 1001;
}

void closeDB(DB_HANDLE handle) {
    std::cout << "Closing database (handle: " << handle << ")\n";
}

struct DBDeleter {
    void operator()(DB_HANDLE* handle) const {
        if (handle) {
            closeDB(*handle);
            delete handle;
        }
    }
};

int main() {
    std::unique_ptr<DB_HANDLE, DBDeleter> db(
        new DB_HANDLE(openDB("mydb")),
        DBDeleter{}
    );
    
    std::cout << "Database managed by unique_ptr\n";
    
    // release() returns the pointer, but does NOT call the deleter
    DB_HANDLE* released = db.release();
    
    std::cout << "Database released from unique_ptr\n";
    
    // WE must manually do what the deleter would do
    closeDB(*released);
    delete released;
    
    return 0;
}

// Output:
// Opening database: mydb
// Database managed by unique_ptr
// Database released from unique_ptr
// Closing database (handle: 1001)

Best Practices for release()

  1. Prefer std::move() when transferring unique_ptr ownership - it’s safer and more explicit:

    // Better than using release()
    ptr2 = std::move(ptr1);
    
  2. Only use release() for true legacy C APIs that require raw pointers

  3. Immediately wrap the pointer if you can’t delete it right away:

    std::unique_ptr<T> new_owner(old_owner.release());
    
  4. Have a clear cleanup plan before calling release()

  5. Document ownership transfer with comments:

    // Transferring ownership to caller
    return buffer.release();
    
  6. Use try-catch when cleanup must happen in exception-prone code:

    try {
        // code that might throw
        delete raw;
    } catch (...) {
        delete raw;  // Cleanup in catch block too
        throw;
    }
    

Summary: What release() Does

AspectBehavior
ReturnsRaw pointer to previously managed object
Ownership TransferYes - transferred to caller
unique_ptr StateBecomes empty (nullptr)
ResponsibilityCaller must clean up the pointer
Deleter AppliedNo - deleter is NOT called
Safe to StoreYes, but you must handle cleanup
Use CaseLegacy C APIs, explicit ownership transfer


Quick Comparison: get() vs release()

Aspectget()release()
What it returnsRaw pointerRaw pointer
OwnershipStays with unique_ptrTransferred to caller
unique_ptr after callStill owns resourceBecomes empty
Who cleans upThe unique_ptrYou must
Deleter calledYes, when unique_ptr destroyedNo
Can store for laterNo - dangerousYes, but risky
Primary useTemporary access to pointerLegacy C APIs requiring ownership
SafetySafe if used correctlyDangerous - manual management
Exception safeYesNo - must handle yourself

Decision Tree: Which Method to Use?

Do you want the unique_ptr to keep managing the resource?
├─ YES  → Use get()
│        (Safe, automatic cleanup)
│
└─ NO   → Use release()
         ├─ Can you immediately wrap in another unique_ptr?
         │  YES → wrap it: std::unique_ptr<T>(old.release())
         │
         └─ NO  → Use release() with legacy C API
                  (Be careful - manual cleanup required)

Key Takeaway

  • get(): “I need to borrow this pointer temporarily while you keep managing it”
  • release(): “I’m taking full responsibility for this pointer and its cleanup”

Choose wisely based on your actual ownership needs!