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:
get()- Get the pointer without changing ownershiprelease()- 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
nullptrif theunique_ptris empty - Ownership: Remains with the
unique_ptr - Responsibility for cleanup: The
unique_ptrstill owns and will clean up the resource - When cleanup happens: When the
unique_ptrgoes 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()
- Use
get()only for temporary access within a limited scope where theunique_ptris still alive - Never store the result of
get()beyond the scope where theunique_ptris valid - Always check for
nullptrbefore dereferencing:if (ptr.get() != nullptr) { // Safe to use } - Prefer
get()for read-only operations on legacy C APIs - Never attempt to delete the pointer returned by
get()- it’s not your responsibility - Document that you’re using
get()- make it clear you’re just borrowing the pointer
Summary: What get() Does
| Aspect | Behavior |
|---|---|
| Returns | Raw pointer to managed object |
| Ownership Transfer | No - remains with unique_ptr |
unique_ptr State | Still owns and manages the resource |
| Responsibility | unique_ptr cleans up on destruction |
| Safe to Store | Only within scope where unique_ptr lives |
| Use Case | Temporary 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
nullptrif theunique_ptrwas already empty - Ownership: Transferred to the caller
unique_ptrstate: 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:
- You must remember to delete it - forgetting causes memory leaks
- You must handle exceptions - if an exception occurs before deletion, you leak memory
- You cannot rely on automatic cleanup - the
unique_ptrwon’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()
-
Prefer
std::move()when transferringunique_ptrownership - it’s safer and more explicit:// Better than using release() ptr2 = std::move(ptr1); -
Only use
release()for true legacy C APIs that require raw pointers -
Immediately wrap the pointer if you can’t delete it right away:
std::unique_ptr<T> new_owner(old_owner.release()); -
Have a clear cleanup plan before calling
release() -
Document ownership transfer with comments:
// Transferring ownership to caller return buffer.release(); -
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
| Aspect | Behavior |
|---|---|
| Returns | Raw pointer to previously managed object |
| Ownership Transfer | Yes - transferred to caller |
unique_ptr State | Becomes empty (nullptr) |
| Responsibility | Caller must clean up the pointer |
| Deleter Applied | No - deleter is NOT called |
| Safe to Store | Yes, but you must handle cleanup |
| Use Case | Legacy C APIs, explicit ownership transfer |
Quick Comparison: get() vs release()
| Aspect | get() | release() |
|---|---|---|
| What it returns | Raw pointer | Raw pointer |
| Ownership | Stays with unique_ptr | Transferred to caller |
unique_ptr after call | Still owns resource | Becomes empty |
| Who cleans up | The unique_ptr | You must |
| Deleter called | Yes, when unique_ptr destroyed | No |
| Can store for later | No - dangerous | Yes, but risky |
| Primary use | Temporary access to pointer | Legacy C APIs requiring ownership |
| Safety | Safe if used correctly | Dangerous - manual management |
| Exception safe | Yes | No - 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!