I skimmed through the documentation on cppreference.com and put down the basic functionalities expected out of a unique_ptr<T> implementation. I coded up a basic implementation std::unique_ptr<T> under 250 lines. This version is not meant to deal with arrays and custom deleters.
I would like to ask someone for a thorough code review? Does my code look like modern C++20/23? Are there any bugs in the code?
Also, how do I support a curly-braced initializer list? I am thinking to code an explicit overload for such cases.
Implementation.
#include <iostream>
#include <memory>
#include <cassert>
#include <utility>
#include <vector>
namespace dev{
// Implement unique_ptr here
template<typename T>
class unique_ptr{
public:
// Default c'tor
unique_ptr() : ptr{nullptr} {}
// Copy c'tor should be deleted to enforce the concept that this is
// an owning pointer
unique_ptr(const unique_ptr& u) = delete;
// Copy assignment should also be deleted to enforce the ownership of the
// managed object
unique_ptr& operator=(const unique_ptr& ) = delete;
// Move constructor
unique_ptr(unique_ptr&& other) : ptr{nullptr}
{
std::swap(ptr, other.ptr);
}
// Move assignment operator
unique_ptr& operator=(unique_ptr&& other){
if(ptr == other)
return (*this);
delete_underlying_ptr();
ptr = std::exchange(other.ptr,nullptr);
return (*this);
}
// Parameterized construtor
unique_ptr(T* p)
: ptr{p}
{}
// Overload deferencing operator *
T& operator*(){
return (*ptr);
}
// get the raw pointer
T* get()
{
return ptr;
}
/* Reset the unique_ptr */
void reset(T* other){
if(ptr == other)
return;
delete_underlying_ptr();
std::swap(ptr, other);
}
/* Release unique_ptr ownership */
T* release(){
return std::exchange(ptr,nullptr);
}
/* Destructor */
~unique_ptr(){
delete_underlying_ptr();
}
/* swap - Exchange the contents of ptr1 and ptr2 member-by-member */
friend void swap(dev::unique_ptr<T>& ptr1, dev::unique_ptr<T>& ptr2) noexcept
{
//Re-wire the raw pointers
std::swap(ptr1.ptr, ptr2.ptr);
}
/* operator bool */
constexpr operator bool(){
return (ptr != nullptr);
}
/* Member access operator - return the underlying pointer */
T* operator->(){
return ptr;
}
/* Helper function to invoke delete on the underlying raw pointer */
void delete_underlying_ptr(){
if(ptr != nullptr){
delete ptr;
ptr = nullptr;
}
}
private:
T* ptr;
};
template<typename T, typename... Args>
T* make_unique(Args&&... args){
return new T(std::forward<Args>(args)...);
}
}
/* Non-member functions */
//Overload operator==
template<typename T1, typename T2>
bool operator==(dev::unique_ptr<T1>& lhs, dev::unique_ptr<T2>& rhs){
return lhs.get() == rhs.get();
}
/*template<typename T1> */
template<typename T1>
bool operator==(dev::unique_ptr<T1>& lhs, std::nullptr_t rhs){
return lhs.get() == nullptr;
}
//Overload operator!=
template<typename T1, typename T2>
bool operator!=(dev::unique_ptr<T1>& lhs, dev::unique_ptr<T2>& rhs){
return !(lhs == rhs);
}
template<typename T1>
bool operator!=(dev::unique_ptr<T1>& lhs, std::nullopt_t& rhs){
return !(lhs == rhs);
}
Test Cases.
int main(){
/* Test Cases*/
{
/* Non member function operator==
Comparison with another unique_ptr or nullptr */
dev::unique_ptr<int> ptr1(new int(10));
dev::unique_ptr<int> ptr2(new int(10));
assert(ptr1 == ptr1);
assert(ptr1 != ptr2);
}
{
/* Create and access */
dev::unique_ptr<int> ptr(new int(10));
assert(ptr != nullptr);
assert(*ptr == 10);
}
{
/* Reset unique_ptr
Replaces the managed object with a new one*/
dev::unique_ptr<int> ptr(new int(10));
ptr.reset(new int(20));
assert(ptr != nullptr);
assert(*ptr == 20);
// Self-reset test
ptr.reset(ptr.get());
}
{
/* Release unique_ptr ownership -
Returns a pointer to the managed object and releases ownership */
dev::unique_ptr<double> ptr(new double(3.14));
double* rawPtr = ptr.release();
assert(ptr == nullptr);
assert(rawPtr != nullptr);
assert(*rawPtr == 3.14);
}
{
/* Non-member function swap
Swap the managed objects */
dev::unique_ptr<int> ptr1(new int(10));
dev::unique_ptr<int> ptr2(new int (20));
swap(ptr1, ptr2);
assert(*ptr1 == 20);
assert(*ptr2 == 10);
}
{
/* Get the raw underlying pointer */
int* x = new int(10);
dev::unique_ptr<int> ptr(x);
int* rawPtr = ptr.get();
assert(*rawPtr == 10);
assert(rawPtr == x);
}
{
/* operator bool to test if the unique pointer owns an object */
dev::unique_ptr<int> ptr(new int(42));
assert(ptr);
ptr.reset(nullptr);
assert(!ptr);
}
{
/* indirection operator* to dereference pointer to managed object,
member access operator -> to call member function*/
struct X{
int n;
int foo(){ return n; }
};
dev::unique_ptr<X> ptr(new X(10));
assert((*ptr).n == 10);
assert(ptr->foo() == 10);
}
{
/* operator[] is used to access a managed array
const int size = 5;
dev::unique_ptr<int[]> fact(new int[size]);
for (int i = 0; i < size; ++i)
fact[i] = (i == 0) ? 1 : i * fact[i - 1];
assert(fact[0] == 1);
assert(fact[1] == 2);
assert(fact[3] == 6);
assert(fact[4] == 24);
assert(fact[5] == 120);
*/
}
{
/* Constructs an object of type T and wraps it in a unique_ptr.*/
struct Point3D{
double x, y, z;
Point3D() {}
Point3D(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {}
Point3D(const Point3D& other) : x(other.x), y(other.y), z(other.z) {}
Point3D(Point3D&& other)
: x(std::move(other.x))
, y(std::move(other.y))
, z(std::move(other.z))
{}
};
// Use the default constructor.
dev::unique_ptr<Point3D> v1 = dev::make_unique<Point3D>();
// Use the constructor that matches these arguments.
dev::unique_ptr<Point3D> v2 = dev::make_unique<Point3D>(0, 1, 2);
// Create a unique_ptr to a vector of 5 elements.
//dev::unique_ptr<std::vector<int>> v3 = dev::make_unique<std::vector<int>>({1,2,3,4,5});
}
}