Took a shot at implementing std::unique_ptr
.
Code:
#include <memory>
#include <stdexcept>
template<typename T>
struct DefaultDeleter {
void operator()(T* ptr) const {
delete ptr;
}
};
template <typename T>
struct DefaultDeleter<T[]> {
void operator()(T* ptr) const {
delete[] ptr;
}
};
template<typename T, typename Deleter = DefaultDeleter<T>>
class UniquePtr {
public:
using pointer = T*;
using element_type = T;
using deleter_type = Deleter;
constexpr UniquePtr() noexcept : m_ptr{nullptr} {}
explicit UniquePtr (pointer p) noexcept : m_ptr{p} {}
explicit UniquePtr (pointer p, const deleter_type& deleter) :
m_ptr{p},
m_deleter{deleter}
{}
UniquePtr(const UniquePtr& other) = delete;
UniquePtr& operator=(const UniquePtr& other) = delete;
UniquePtr(UniquePtr&& other) noexcept :
m_ptr{other.release()},
m_deleter{std::move(other.m_deleter)}
{}
// Added this to allow for runtime polymorphism, but I didn't
// see such a constructor on cppreference. Am I just missing something
// or should this be handled another way?
template<typename U,
std::enable_if_t<std::is_convertible_v<U*, T*>, bool> = false>
UniquePtr(UniquePtr<U>&& other) noexcept :
m_ptr{other.release()}
{}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
m_deleter(m_ptr);
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
m_deleter = std::move(other.m_deleter);
}
return *this;
}
pointer release() noexcept {
pointer result = m_ptr;
m_ptr = nullptr;
return result;
}
void reset(pointer ptr = pointer()) noexcept {
pointer old_ptr = m_ptr;
m_ptr = ptr;
if (old_ptr) {
m_deleter(old_ptr);
}
}
void swap(UniquePtr& other) noexcept {
std::swap(m_ptr, other.ptr_);
std::swap(m_deleter, other.m_deleter);
}
pointer get() const noexcept {
return m_ptr;
}
explicit operator bool() const noexcept {
return get() != nullptr;
}
std::add_lvalue_reference_t<element_type> operator* () const
noexcept(noexcept(*std::declval<pointer>())) {
return *m_ptr;
}
pointer operator->() const noexcept {
return m_ptr;
}
~UniquePtr() {
m_deleter(m_ptr);
}
private:
pointer m_ptr;
deleter_type m_deleter;
};
template<typename T, typename... Args>
UniquePtr<T> makeUnique(Args&&... args) {
return UniquePtr<T>(new T(std::forward<Args>(args)...));
}
Some tests, mostly attempting to show the example functionality on cppreference.com works with my implementation. Plus a couple extra tests to fill in gaps/test things a bit more precisely.
#include <cassert>
#include <fstream>
#include <iostream>
#include "UniquePtr.h"
/* Test setup code (adapted from cppreference.com) */
// helper class for runtime polymorphism demo below
struct B
{
virtual ~B() = default;
virtual int val() const {return 0;}
};
struct D : B
{
D() { }
~D() {}
int val() const override {
// dummy value to demonstrate dereferencing
return 2;
}
};
// a function consuming a unique_ptr can take it by value or by rvalue reference
UniquePtr<D> pass_through(UniquePtr<D> p)
{
p->val();
return p;
}
// helper function for the custom deleter demo below
void close_file(std::FILE* fp)
{
std::fclose(fp);
}
struct Node
{
int data;
UniquePtr<Node> next;
};
// unique_ptr-based linked list demo
struct List
{
UniquePtr<Node> head;
~List()
{
// destroy list nodes sequentially in a loop, the default destructor
// would have invoked its `next`'s destructor recursively, which would
// cause stack overflow for sufficiently large lists.
while (head)
{
// std::cout << "before first move \n";
auto next = std::move(head->next);
// std::cout << "before second move \n";
head = std::move(next);
}
}
void push(int data)
{
head = UniquePtr<Node>(new Node{data, std::move(head)});
}
};
/* Beginning of tests */
void testOwnershipTransfer() {
UniquePtr<D> p = makeUnique<D>();
UniquePtr<D> q = pass_through(std::move(p));
assert(!p);
}
void testNullUniquePtr() {
UniquePtr<D> p;
} // this function should exit successfully
void testDererference() {
UniquePtr<D> p = makeUnique<D>();
D obj = *p;
assert(obj.val() == 2);
}
void testBool() {
UniquePtr<D> p;
assert(!p);
p = makeUnique<D>();
assert(p);
p.reset();
assert(!p);
}
void testRuntimePolymorphism() {
UniquePtr<B> p = makeUnique<D>();
assert(p -> val() == 2);
}
void testFileDeletion() {
std::ofstream("test.txt") << 'x'; // prepare the file to read
{
using unique_file_t = UniquePtr<std::FILE, decltype(&close_file)>;
unique_file_t fp(std::fopen("test.txt", "r"), &close_file);
if (fp) {
assert(char(std::fgetc(fp.get())) == 'x');
}
} // `close_file()` called here (if `fp` is not null)
}
void testExceptionSafety() {
try
{
UniquePtr<D, void(*)(D*)> p(new D, [](D* ptr)
{
delete ptr;
});
throw std::runtime_error(""); // `p` would leak here if it were a plain pointer
}
catch (const std::exception&) { std::cout << "Caught exception\n"; }
}
void testLinkedList() {
List wall;
for (int i = 0; i != 1'000'000; ++i)
wall.push(i);
} // function should exit successfully
int main()
{
testOwnershipTransfer();
testNullUniquePtr();
testDererference();
testBool();
testRuntimePolymorphism();
testFileDeletion();
testExceptionSafety();
testLinkedList();
}
This omits the array specialization, because when taking a peek at the GCC implementation it seems to be mostly a duplication of the class with some variations here and there to support arrays. Is this how it needs to be done, or is it possible to reuse the existing code somehow and write a small, concise class for the array specialization?
Thanks in advance to anyone who can take a look!