Introduction
In this post, I try to write a simple homegrown version of std::unique_ptr<T>
. This post is partly inspired by the fantastic book C++ Memory Management by Patrice Roy. Tghe toy examples in this book are very instructive and I highly reckon you order a copy. Our goal is just to build intuition for the kind of code required to write such a type, and not to try and replace the standard library facilities.
The std::unique_ptr<T>
smart pointer type models unqiue(sole) ownership of the resource semantics.
struct X{};
std::unique_ptr<X> p1 = std::make_unique<X>();
//std::unique_ptr<X> p2(p1); // Error when calling copy constructor,
// p1 is the exclusive owner
std::unique_ptr
enforces exclusive ownership using the fact, that it is not copy-constructible or copy-assignable. Note however, that it doesn’t prevent you from writing deliberately hostile code. The below code is compiles perfectly well and is valid C++.
The copy constructor and the copy assignment operator of std::unique_ptr<T>
are marked delete
. It is however, move constructible and move-assignable.
Basic functionalities to expect out of std::unique_ptr<T>
I skimmed through the documentation for std::unique_ptr
on cppreference.com. A basic implementation of unique_ptr
in less than 200 lines of code should pass the following unit tests:
#include <gtest/gtest.h>
#include "unique_ptr.h"
TEST(UniquePtrTest, CreateAndAccessTest)
{
int* raw_ptr = new int(42);
dev::unique_ptr<int> p1(raw_ptr);
EXPECT_EQ(*p1 == 42,true);
EXPECT_EQ(p1.get(), raw_ptr);
dev::unique_ptr<int> p2 = new int(17);
EXPECT_EQ(*p2 == 17,true);
EXPECT_EQ(p2.get() != nullptr, true);
}
/* Move constructor - Transfer of ownership */
TEST(UniquePtrTest, MoveConstructorTest)
{
dev::unique_ptr p {dev::unique_ptr(new int(17))};
EXPECT_EQ(*p, 17);
EXPECT_EQ(p!=nullptr, true);
}
/* Move assignment */
TEST(UniquePtrTest, MoveAssignmentTest)
{
dev::unique_ptr<int> p1(new int(42));
p1 = dev::unique_ptr<int>(new int(17));
EXPECT_EQ(p1!=nullptr, true);
EXPECT_EQ(*p1 == 17, true);
}
// Modifiers
/* release() : Returns the pointer to resource and releases ownership*/
TEST(UniquePtrTest, ReleaseTest){
dev::unique_ptr<double> ptr(new double(3.14));
double* rawPtr = ptr.release();
EXPECT_EQ(ptr == nullptr, true);
EXPECT_EQ(rawPtr != nullptr, true);
EXPECT_EQ(*rawPtr == 3.14, true);
}
/* reset() : replaces the managed object */
TEST(UniquePtrTest, ResetUniquePtr) {
dev::unique_ptr<int> ptr(new int(10));
ptr.reset(new int(20));
EXPECT_EQ(ptr != nullptr, true);
EXPECT_EQ(*ptr == 20, true);
// Self-reset test
ptr.reset(ptr.get());
}
/* swap() : swap the managed objects */
TEST(UniquePtrTest, SwapTest){
int* first = new int(42);
int* second = new int(17);
dev::unique_ptr<int> p1(first);
dev::unique_ptr<int> p2(second);
swap(p1, p2);
EXPECT_EQ(p2.get() == first && p1.get() == second, true);
EXPECT_EQ(((*p1) == 17) && ((*p2) == 42), true);
}
// Observers
/* get() : Returns a pointer to the managed object or nullptr*/
TEST(UniquePtrTest, GetTest){
double* resource = new double(0.50);
dev::unique_ptr p(resource);
EXPECT_EQ(p.get() == resource, true);
EXPECT_EQ(*(p.get()) == 0.50, true);
}
/* operator bool() : Checks whether *this owns an object*/
/*TEST(UniquePtrTest, OperatorBoolTest){
int* resource = new int(28);
dev::unique_ptr<int> p1;
dev::unique_ptr<int> p2(resource);
EXPECT_EQ(p1, false);
EXPECT_EQ(p2, true);
}*/
// Pointer-like functions
TEST(UniquePtrTest, IndirectionOperatorTest) {
/* indirection operator* to dereference pointer to managed object,
member access operator -> to call member function*/
struct X {
int _n;
X() = default;
X(int n) : _n{n} {}
~X() = default;
int foo() { return _n; }
};
dev::unique_ptr<X> ptr(new X(10));
EXPECT_EQ((*ptr)._n == 10, true);
EXPECT_EQ(ptr->foo() == 10, true);
}
TEST(UniquePtrTest, PointerToArrayOfTConstructionAndAccess){
/* Constructing unique_ptr<T[]> and access */
dev::unique_ptr<int[]> p;
EXPECT_EQ(p == nullptr, true);
{
p = new int[5]{1, 2, 3, 4, 5};
EXPECT_EQ(p !=nullptr, true);
EXPECT_EQ(*p == 1, true);
EXPECT_EQ(p[2] == 3, true);
p.release();
EXPECT_EQ(p ==nullptr, true);
}
}
A custom implementation
Most of the code is self-evident. The special case, where the resource(pointee) is an array of elements of type T
, we write a partial specialization of unique_ptr<T[],D>
.
#include<format>
#include<concepts>
namespace dev{
/* Default deleter - single object version */
template<typename T>
struct default_deleter{
void operator()(T* raw_ptr){
delete raw_ptr;
}
};
/* Default deleter - pointee is an array of objects version */
template<typename T>
struct default_deleter<T[]>{
void operator()(T* raw_ptr){
delete[] raw_ptr;
}
};
/* Single object version */
template<typename T, typename D = default_deleter<T>>
class unique_ptr : public D{
public:
using deleter_type = D;
/* Default c'tor */
unique_ptr() = default;
unique_ptr(const unique_ptr& ) = delete;
unique_ptr& operator=(const unique_ptr& ) = delete;
/* Parameteric constructor */
unique_ptr(T* ptr) : m_underlying_ptr{ptr}
{}
/* Swap function */
void swap(unique_ptr& other) noexcept{
std::swap(m_underlying_ptr, other.m_underlying_ptr);
}
/* Move constructor */
unique_ptr(unique_ptr&& other)
: m_underlying_ptr{ std::move(other.m_underlying_ptr)}
{}
/* Move assignment */
unique_ptr& operator=(unique_ptr&& other){
std::swap(m_underlying_ptr, other.m_underlying_ptr);
return *this;
}
/* Destructor */
~unique_ptr(){
deleter_type* deleter = static_cast<deleter_type*>(this);
(*deleter)(m_underlying_ptr);
}
// Pointer-like functions
/* Dereferencing operator */
T operator*(){
return *m_underlying_ptr;
}
/* Indirection operator*/
T* operator->(){
return m_underlying_ptr;
}
/* get() - get the raw underlying pointer*/
T* get() const{
return m_underlying_ptr;
}
// Modifiers
/* Release - returns a pointer to the managed object
and releases the ownership*/
T* release(){
return std::exchange(m_underlying_ptr, nullptr);
}
/* Reset - Replaces the managed object */
void reset(T* other){
if(m_underlying_ptr != other)
{
deleter_type* deleter = static_cast<deleter_type*>(this);
(*deleter)(m_underlying_ptr);
m_underlying_ptr = other;
}
}
void reset(std::nullptr_t){
deleter_type* deleter = static_cast<deleter_type*>(this);
(*deleter)(m_underlying_ptr);
m_underlying_ptr = nullptr;
}
explicit operator bool() const{
return (m_underlying_ptr == nullptr);
}
friend bool operator==(const unique_ptr& lhs, const unique_ptr& rhs){
return (lhs.get() == rhs.get());
}
friend bool operator==(const unique_ptr& lhs, std::nullptr_t rhs){
return (lhs.m_underlying_ptr == nullptr);
}
friend bool operator!=(unique_ptr& lhs, unique_ptr& rhs){
return !(lhs == rhs);
}
friend bool operator!=(unique_ptr& lhs, std::nullptr_t){
return !(lhs == nullptr);
}
friend void swap(unique_ptr& lhs, unique_ptr& rhs){
lhs.swap(rhs);
}
private:
T* m_underlying_ptr{nullptr};
};
/* Array version*/
template<typename T, typename D>
class unique_ptr<T[], D> : public D{
public:
using deleter_type = D;
/* Default c'tor */
unique_ptr() = default;
unique_ptr(const unique_ptr& ) = delete;
unique_ptr& operator=(const unique_ptr& ) = delete;
/* Parameteric constructor */
unique_ptr(T* ptr) : m_underlying_ptr{ptr}
{}
/* Swap function */
void swap(unique_ptr& other) noexcept{
std::swap(m_underlying_ptr, other.m_underlying_ptr);
}
/* Move constructor */
unique_ptr(unique_ptr&& other)
: m_underlying_ptr{ std::move(other.m_underlying_ptr)}
{}
/* Move assignment */
unique_ptr& operator=(unique_ptr&& other){
std::swap(m_underlying_ptr, other.m_underlying_ptr);
return *this;
}
/* Destructor */
~unique_ptr(){
deleter_type* deleter = static_cast<deleter_type*>(this);
(*deleter)(m_underlying_ptr);
}
// Pointer-like functions
/* Dereferencing operator */
T operator*(){
return *m_underlying_ptr;
}
/* Indirection operator*/
T* operator->(){
return m_underlying_ptr;
}
/* IndexOf operator - provides indexed access
to the managed array.*/
T operator[](std::size_t index){
return m_underlying_ptr[index];
}
/* get() - get the raw underlying pointer*/
T* get() const{
return m_underlying_ptr;
}
// Modifiers
/* Release - returns a pointer to the managed object
and releases the ownership*/
T* release(){
return std::exchange(m_underlying_ptr, nullptr);
}
/* Reset - Replaces the managed object */
void reset(T* other){
if(m_underlying_ptr != other)
{
deleter_type* deleter = static_cast<deleter_type*>(this);
(*deleter)(m_underlying_ptr);
m_underlying_ptr = other;
}
}
void reset(std::nullptr_t){
deleter_type* deleter = static_cast<deleter_type*>(this);
(*deleter)(m_underlying_ptr);
m_underlying_ptr = nullptr;
}
explicit operator bool() const{
return (m_underlying_ptr == nullptr);
}
friend bool operator==(const unique_ptr& lhs, const unique_ptr& rhs){
return (lhs.get() == rhs.get());
}
friend bool operator==(const unique_ptr& lhs, std::nullptr_t rhs){
return (lhs.m_underlying_ptr == nullptr);
}
friend bool operator!=(unique_ptr& lhs, unique_ptr& rhs){
return !(lhs == rhs);
}
friend bool operator!=(unique_ptr& lhs, std::nullptr_t){
return !(lhs == nullptr);
}
friend void swap(unique_ptr& lhs, unique_ptr& rhs){
lhs.swap(rhs);
}
private:
T* m_underlying_ptr{nullptr};
};
}
You can play around the code files, build the project and run unit tests for this (naive) toy-implementation of unique_ptr
by cloning my GitHub repo github.com/quasar-chunawala/interview_data_structures.
References
- C++ Memory Management by Patrice Roy*.