Smart Pointers Overview
pointers.hpp provides three smart pointer types that implement automatic
memory management with RAII semantics. The library includes UniquePtr
for exclusive ownership, SharedPtr for shared ownership with thread-safe
reference counting, and WeakPtr for non-owning observation. All
implementations closely mirror the C++ standard library’s smart pointer behavior.
Design Philosophy
The smart pointer library follows these core principles:
RAII Semantics: Automatic resource management through object lifetime
Type-Safe: Extensive use of SFINAE to enforce correct usage at compile-time
Zero-Overhead Unique Ownership:
UniquePtrhas no runtime cost compared to raw pointersThread-Safe Shared Ownership:
SharedPtruses atomic reference counting for safe concurrent accessStandard-Compliant: Closely mirrors
std::unique_ptr,std::shared_ptr, andstd::weak_ptrCustomizable Deletion: Support for custom deleters and stateful cleanup
Library Components
Smart Pointer Types
UniquePtr<T, Deleter>
Exclusive ownership smart pointer representing unique ownership of a dynamically allocated object.
Key Features:
Zero overhead compared to raw pointers (with default deleter)
Move-only semantics (non-copyable)
Custom deleters by value or by reference
Separate array specialization
UniquePtr<T[]>Converting constructors for polymorphic types
Complete set of comparison operators
Size: Single pointer (with default deleter), or pointer + deleter size
WeakPtr<T>
Non-owning observer that doesn’t affect object lifetime.
Key Features:
Observes
SharedPtr-managed objects without ownershipThread-safe promotion to
SharedPtrvialock()Automatically expires when referenced object is destroyed
Essential for breaking reference cycles
Supports type conversions (derived-to-base)
Size: Two pointers (same as SharedPtr)
Supporting Components
DefaultDelete<T>
Default deleter that calls delete on the pointer.
template <class T>
struct DefaultDelete {
void operator()(T* ptr) const noexcept {
delete ptr;
}
};
Array Specialization: DefaultDelete<T[]> calls delete[]
DeleterStorage<Deleter, IsRef>
Internal storage mechanism for deleters in UniquePtr.
Value Deleters: Stores deleter by value Reference Deleters: Stores pointer to external deleter
This allows UniquePtr to support both stateless and stateful deleters efficiently.
Helper Traits
HasPointerType<D>: Detects if deleter defines custom ::pointer type
PointerType<T, D>: Resolves to D::pointer if available, otherwise T*
These enable support for “fancy pointers” (custom pointer types).
UniquePtr Details
Basic Usage
#include "pointers.hpp"
// Basic construction
cslt::UniquePtr<int> p1(new int(42));
// Factory function (recommended)
auto p2 = cslt::make_unique<int>(42);
// Access
*p2 = 99;
int value = *p2; // 99
Ownership Semantics
UniquePtr represents exclusive ownership and cannot be copied:
cslt::UniquePtr<int> p1 = cslt::make_unique<int>(42);
// cslt::UniquePtr<int> p2 = p1; // ERROR: copy is deleted
cslt::UniquePtr<int> p2 = cslt::move(p1); // OK: move transfers ownership
// p1 is now null, p2 owns the object
Custom Deleters
Value Deleters
Store the deleter by value in the UniquePtr:
struct FileCloser {
void operator()(FILE* f) const {
if (f) fclose(f);
}
};
cslt::UniquePtr<FILE, FileCloser> file(fopen("data.txt", "r"));
// File automatically closed when file goes out of scope
Reference Deleters
Store a reference to an external deleter:
struct LoggingDeleter {
int count = 0;
void operator()(int* p) {
++count;
delete p;
}
};
LoggingDeleter deleter;
{
cslt::UniquePtr<int, LoggingDeleter&> p(new int(42), deleter);
}
// deleter.count is now 1
Array Specialization
UniquePtr<T[]> provides array-specific operations:
// Array construction
cslt::UniquePtr<int[]> arr = cslt::make_unique_array<int>(100);
// Array access
arr[0] = 42;
arr[99] = 99;
// Automatic cleanup with delete[]
Key Differences from Single-Object:
Uses
operator[]instead ofoperator*andoperator->Deleter calls
delete[]instead ofdeleteNo converting constructors between array and non-array types
Type Conversions
UniquePtr supports converting moves from derived to base types:
struct Base { virtual ~Base() = default; };
struct Derived : Base { };
cslt::UniquePtr<Derived> derived = cslt::make_unique<Derived>();
cslt::UniquePtr<Base> base = cslt::move(derived);
// derived is now null, base owns the Derived object
Member Functions
Constructors
UniquePtr(); // Default construct (null)
UniquePtr(nullptr_t); // Null pointer
explicit UniquePtr(pointer p); // Take ownership of p
UniquePtr(pointer p, const Deleter& d); // With deleter copy
UniquePtr(pointer p, Deleter&& d); // With deleter move
UniquePtr(UniquePtr&& other); // Move constructor
template <class U, class D2>
UniquePtr(UniquePtr<U, D2>&& other); // Converting move
Observers
pointer get() const noexcept; // Get raw pointer
Deleter& get_deleter() noexcept; // Access deleter
explicit operator bool() const noexcept; // Test if non-null
T& operator*() const noexcept; // Dereference
T* operator->() const noexcept; // Member access
T& operator[](size_t i) const noexcept; // Array subscript (array specialization only)
Modifiers
pointer release() noexcept; // Release ownership, return pointer
void reset(pointer p = pointer()) noexcept; // Replace managed object
void swap(UniquePtr& other) noexcept; // Exchange contents
Comparison Operators
template <class T1, class D1, class T2, class D2>
bool operator==(const UniquePtr<T1, D1>&, const UniquePtr<T2, D2>&);
bool operator!=(const UniquePtr<T1, D1>&, const UniquePtr<T2, D2>&);
bool operator<(const UniquePtr<T1, D1>&, const UniquePtr<T2, D2>&);
bool operator<=(const UniquePtr<T1, D1>&, const UniquePtr<T2, D2>&);
bool operator>(const UniquePtr<T1, D1>&, const UniquePtr<T2, D2>&);
bool operator>=(const UniquePtr<T1, D1>&, const UniquePtr<T2, D2>&);
// Comparisons with nullptr
bool operator==(const UniquePtr<T, D>&, nullptr_t);
bool operator==(nullptr_t, const UniquePtr<T, D>&);
bool operator!=(const UniquePtr<T, D>&, nullptr_t);
bool operator!=(nullptr_t, const UniquePtr<T, D>&);
Factory Functions
template <class T, class... Args>
UniquePtr<T> make_unique(Args&&... args);
template <class T>
UniquePtr<T[]> make_unique_array(size_t n);
WeakPtr Details
Basic Usage
#include "pointers.hpp"
cslt::SharedPtr<int> shared = cslt::make_shared<int>(42);
cslt::WeakPtr<int> weak = shared;
// Check if still valid
if (!weak.expired()) {
cslt::SharedPtr<int> locked = weak.lock();
if (locked) {
std::cout << *locked; // 42
}
}
Purpose and Use Cases
WeakPtr solves several important problems:
Breaking Reference Cycles
Prevents memory leaks in circular references:
struct Node {
cslt::SharedPtr<Node> next; // Strong reference
cslt::WeakPtr<Node> prev; // Weak reference (breaks cycle)
};
auto n1 = cslt::make_shared<Node>();
auto n2 = cslt::make_shared<Node>();
n1->next = n2;
n2->prev = n1; // Weak pointer prevents cycle
Cache Implementation
Allows objects to be cached without preventing cleanup:
std::map<std::string, cslt::WeakPtr<Resource>> cache;
cslt::SharedPtr<Resource> get_resource(const std::string& key) {
auto it = cache.find(key);
if (it != cache.end()) {
if (auto locked = it->second.lock()) {
return locked; // Cache hit
}
}
// Cache miss, create new
auto resource = cslt::make_shared<Resource>(key);
cache[key] = resource;
return resource;
}
Observer Pattern
Allows observers without preventing subject destruction:
class Subject {
std::vector<cslt::WeakPtr<Observer>> observers_;
public:
void notify() {
for (auto& weak : observers_) {
if (auto obs = weak.lock()) {
obs->update();
}
}
}
};
Expiration
WeakPtr automatically expires when the referenced object is destroyed:
cslt::WeakPtr<int> weak;
{
auto shared = cslt::make_shared<int>(42);
weak = shared;
std::cout << weak.expired(); // false
}
// shared destroyed here
std::cout << weak.expired(); // true
auto locked = weak.lock();
std::cout << (locked == nullptr); // true
Thread-Safe Promotion
WeakPtr::lock() is thread-safe and atomically promotes to SharedPtr:
cslt::SharedPtr<Data> shared = cslt::make_shared<Data>();
cslt::WeakPtr<Data> weak = shared;
// Thread 1
shared.reset(); // May destroy object
// Thread 2
auto locked = weak.lock(); // Thread-safe
// Either gets valid SharedPtr or null, never dangling pointer
The implementation uses compare_exchange_weak in a loop to atomically check
if the object is still alive and increment the reference count if so.
Member Functions
Constructors
WeakPtr(); // Default construct (empty)
WeakPtr(const SharedPtr<T>& sp); // Construct from SharedPtr
template <class U>
WeakPtr(const SharedPtr<U>& sp); // Converting construct from SharedPtr
WeakPtr(const WeakPtr& other); // Copy constructor
template <class U>
WeakPtr(const WeakPtr<U>& other); // Converting copy
WeakPtr(WeakPtr&& other); // Move constructor
template <class U>
WeakPtr(WeakPtr<U>&& other); // Converting move
Observers
size_t use_count() const noexcept; // Get strong reference count
bool expired() const noexcept; // Test if object destroyed
SharedPtr<T> lock() const noexcept; // Try to create SharedPtr
Modifiers
void reset() noexcept; // Release weak reference
void swap(WeakPtr& other) noexcept; // Exchange contents
Control Block Lifetime
The control block survives as long as either strong or weak references exist:
cslt::WeakPtr<int> weak;
{
auto shared = cslt::make_shared<int>(42);
weak = shared;
}
// Object destroyed, but control block still exists
weak.reset();
// Control block now destroyed
This ensures WeakPtr can safely detect expiration without accessing freed memory.
Usage Examples
Basic Ownership Patterns
Unique Ownership
#include "pointers.hpp"
void process_data() {
auto data = cslt::make_unique<Data>();
data->process();
// Automatic cleanup when function returns
}
Polymorphism
#include "pointers.hpp"
#include <vector>
struct Shape { virtual void draw() = 0; virtual ~Shape() = default; };
struct Circle : Shape { void draw() override { /* ... */ } };
struct Square : Shape { void draw() override { /* ... */ } };
std::vector<cslt::UniquePtr<Shape>> shapes;
shapes.push_back(cslt::make_unique<Circle>());
shapes.push_back(cslt::make_unique<Square>());
for (auto& shape : shapes) {
shape->draw(); // Polymorphic call
}
RAII Resource Management
#include "pointers.hpp"
struct FileDeleter {
void operator()(FILE* f) const {
if (f) fclose(f);
}
};
void process_file(const char* filename) {
cslt::UniquePtr<FILE, FileDeleter> file(fopen(filename, "r"));
if (!file) {
throw std::runtime_error("Failed to open file");
}
// Use file...
// Automatic close on all exit paths (return, exception, etc.)
}
Factory Pattern
#include "pointers.hpp"
struct Widget {
virtual ~Widget() = default;
virtual void action() = 0;
};
struct ButtonWidget : Widget {
void action() override { /* ... */ }
};
struct SliderWidget : Widget {
void action() override { /* ... */ }
};
cslt::UniquePtr<Widget> create_widget(const std::string& type) {
if (type == "button") {
return cslt::make_unique<ButtonWidget>();
} else if (type == "slider") {
return cslt::make_unique<SliderWidget>();
}
return nullptr;
}
Doubly Linked List
#include "pointers.hpp"
template <class T>
struct Node {
T data;
cslt::SharedPtr<Node> next; // Strong reference (owns)
cslt::WeakPtr<Node> prev; // Weak reference (observes)
Node(const T& value) : data(value) {}
};
template <class T>
class List {
cslt::SharedPtr<Node<T>> head_;
cslt::WeakPtr<Node<T>> tail_;
public:
void push_back(const T& value) {
auto new_node = cslt::make_shared<Node<T>>(value);
if (auto tail = tail_.lock()) {
tail->next = new_node;
new_node->prev = tail_;
} else {
head_ = new_node;
}
tail_ = new_node;
}
};
Caching System
#include "pointers.hpp"
#include <map>
#include <string>
class ResourceCache {
std::map<std::string, cslt::WeakPtr<Resource>> cache_;
public:
cslt::SharedPtr<Resource> get(const std::string& key) {
// Try cache first
auto it = cache_.find(key);
if (it != cache_.end()) {
if (auto cached = it->second.lock()) {
return cached; // Cache hit
}
// Expired, remove from cache
cache_.erase(it);
}
// Cache miss, load resource
auto resource = cslt::make_shared<Resource>(key);
cache_[key] = resource;
return resource;
}
void clear() {
cache_.clear();
}
};
Performance Characteristics
UniquePtr Performance
Size: * Default deleter: sizeof(T*) * Custom value deleter: sizeof(T*) + sizeof(Deleter) * Reference deleter: sizeof(T*)
Operations: * Construction: O(1) * Destruction: O(1) + deleter cost * Move: O(1) * Copy: Not available * Comparison: O(1)
Overhead: Zero runtime overhead with default deleter
WeakPtr Performance
Size: * Same as SharedPtr: 2 * sizeof(void*)
Operations: * Construction: O(1) + atomic increment (weak count) * Copy: O(1) + atomic increment * Destruction: O(1) + atomic decrement * Move: O(1) * lock(): O(1) + atomic compare-and-swap loop * expired(): O(1) + atomic load
Overhead: * Minimal - weak references don’t affect object lifetime * lock() may retry in highly contended scenarios
Memory Layout
UniquePtr Memory Layout (Default Deleter)
UniquePtr<T>: [T* ptr]
Heap: [T object]
WeakPtr Memory Layout
WeakPtr<T>: [T* ptr] [ControlBlock* cb]
(Shares control block with SharedPtr)
Comparison with Standard Library
This implementation closely follows the C++ standard library:
csalt++ Component |
Standard Library Equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Key Differences
SharedPtr / make_shared:
Uses two allocations (object + control block) instead of one
Simpler implementation, slight performance cost
Control block structure is visible in this implementation
Missing Features:
enable_shared_from_this<T>- Safe sharing ofthisweak_ptr::owner_before()- Ordering by ownershipAllocator support for
SharedPtrunique_ptrhash supportArray support for
SharedPtr(C++17)
Thread Safety:
Reference counting is always atomic (no separate
atomic_shared_ptr)Same thread-safety guarantees as standard library
Requirements
Compiler Support
C++11 or later
Support for variadic templates
Support for template aliases
Support for
constexprandnoexceptSupport for
std::atomicSupport for deleted functions (
= delete)
Dependencies
utilities.hpp- Type traits, move/forward, reference counting<cstddef>- Forsize_t
Best Practices
Use UniquePtr by Default
Start with UniquePtr and only use SharedPtr when shared ownership is actually needed:
// Good: clear ownership
cslt::UniquePtr<Resource> create_resource() {
return cslt::make_unique<Resource>();
}
// Only use SharedPtr when truly needed
cslt::SharedPtr<Cache> global_cache = cslt::make_shared<Cache>();
Check WeakPtr Before Use
Always check expired() or test the result of lock():
cslt::WeakPtr<Resource> weak = /* ... */;
// Method 1: Check expired
if (!weak.expired()) {
auto shared = weak.lock();
// Use shared...
}
// Method 2: Check lock result
if (auto shared = weak.lock()) {
// Use shared...
}
Don’t Mix Ownership Models
Avoid creating multiple SharedPtr instances from the same raw pointer:
// Bad: double delete!
T* raw = new T();
cslt::SharedPtr<T> p1(raw);
cslt::SharedPtr<T> p2(raw); // WRONG: two control blocks
// Good: share through copy
cslt::SharedPtr<T> p1 = cslt::make_shared<T>();
cslt::SharedPtr<T> p2 = p1; // Correct: shared control block
Common Pitfalls
Dangling References from get()
T* raw;
{
auto ptr = cslt::make_unique<T>();
raw = ptr.get(); // Get raw pointer
}
// ptr destroyed, raw is now dangling!
// *raw; // Undefined behavior
Solution: Keep the smart pointer alive as long as you need access.
Forgetting to Move UniquePtr
cslt::UniquePtr<T> create() {
auto ptr = cslt::make_unique<T>();
return ptr; // Implicitly moved (OK)
}
void transfer(cslt::UniquePtr<T> dest, cslt::UniquePtr<T> src) {
dest = src; // ERROR: copy not allowed
dest = cslt::move(src); // OK
}
Future Enhancements
Potential additions for future versions:
enable_shared_from_this<T>- Safethissharing in member functionsSingle-allocation
make_sharedoptimizationAllocator support for custom memory allocation
atomic_shared_ptrandatomic_weak_ptrwrappersArray support for
SharedPtr(SharedPtr<T[]>)unique_ptrhash specializationC++17/20 features (deduction guides,
std::spanintegration)observer_ptr- Non-owning raw pointer wrapper