value_ptr
Sep 28, 2017In today’s blog post, we’re going to discuss the idea of a value_ptr
.
Basically, the idea is that it’s pointer with value semantics.
Why would you want this?
Well, polymorphism for one.
For another, you could conceivably want to allocate a very large number of some very large objects on the stack (perhaps recursively), and you don’t want to cause an overflow.
This has been implemented before, but I didn’t actually read the source for that. Instead, I’ll write it myself.
Part 1: Initial Attempt
Here was my initial attempt:
namespace nmh {
template<typename T>
class value_ptr {
private:
T* pointedAt;
public:
value_ptr(const value_ptr<T>& other) {
pointedAt = new T(other.pointedAt);
}
// note that assigning to a moved-from object is now undefined
// behavior, as it will dereference null
value_ptr(value_ptr<T>&& other) {
pointedAt = other.pointedAt;
other.pointedAt = nullptr;
}
template<typename ...Args>
value_ptr(Args && ...args) {
pointedAt = new T(std::forward<Args>(args)...);
}
value_ptr<T>& operator=(const value_ptr<T>& other) {
*pointedAt = *other.pointedAt;
return *this;
}
T* operator->() {
return pointedAt;
}
~value_ptr() {
if(pointedAt == nullptr) {
// nothing for now!
}
else {
delete pointedAt;
}
}
};
}
I tested this with a simple test program:
#include <iostream>
#include "value_ptr.hpp"
using nmh::value_ptr;
struct foo {
int x;
int y;
foo(int _x, int _y) : x(_x), y(_y) {}
foo(const foo&) = default;
};
int addMembers(value_ptr<foo> in) {
return in->x + in->y;
}
int main() {
nmh::value_ptr<foo> ptr(1, 2);
std::cout << ptr->x << "\t" << ptr->y << std::endl;
int sum = addMembers(ptr);
std::cout << sum << std::endl;
}
However, I kept getting this compiler error:
In file included from test.cpp:2:
./value_ptr.hpp:22:29: error: no matching constructor for initialization of
'foo'
pointedAt = new T(std::forward<Args>(args)...);
^ ~~~~~~~~~~~~~~~~~~~~~~~~
test.cpp:20:26: note: in instantiation of function template specialization
'nmh::value_ptr<foo>::value_ptr<nmh::value_ptr<foo> &>' requested here
int sum = addMembers(ptr);
^
test.cpp:10:5: note: candidate constructor not viable: no known conversion from
'nmh::value_ptr<foo>' to 'const foo' for 1st argument
foo(const foo&) = default;
^
test.cpp:9:5: note: candidate constructor not viable: requires 2 arguments, but
1 was provided
foo(int _x, int _y) : x(_x), y(_y) {}
^
1 error generated.
This was very confusing at first.
However, it turns out that the template argument will have precedence over any of the constructors I defined.
Why?
Well, for some reason, the compiler would not automatically convert the copy-constructor argument value_ptr<T>&
to const value_ptr<T>&
.
I suspect this is because it prefers to use a template than to do any conversion, no matter how trivial.
Thankfully, this is an easy fix:
namespace nmh {
template<typename T>
class value_ptr {
private:
T* pointedAt;
public:
value_ptr(value_ptr<T> const & other) {
pointedAt = new T(*other.pointedAt);
}
value_ptr(value_ptr<T>& other) {
pointedAt = new T(*other.pointedAt);
}
// note that assigning to a moved-from object is now undefined
// behavior, as it will dereference null
value_ptr(value_ptr<T>&& other) {
pointedAt = other.pointedAt;
other.pointedAt = nullptr;
}
template<typename ...Args>
value_ptr(Args && ...args) {
pointedAt = new T(std::forward<Args>(args)...);
}
// other stuff
};
}
2: Testing Moves
Let’s make sure moves work, shall we? We modify the test program again:
// Previous values
void testMove() {
value_ptr<foo> f(10, 11);
value_ptr<foo> y = std::move(f);
std::cout << y->x << std::endl;
}
int main() {
nmh::value_ptr<foo> ptr(1, 2);
std::cout << ptr->x << "\t" << ptr->y << std::endl;
int sum = addMembers(ptr);
std::cout << sum << std::endl;
testMove();
}
We also change the destructor of value-ptr to check:
~value_ptr() {
if(pointedAt == nullptr) {
std::cout << "Destructing a moved-from object" << std::endl;
// nothing for now!
}
else {
delete pointedAt;
}
}
To what I must admit is my surprise, this has actually worked! That’s pretty nifty.
3: Polymorphism
This is where things get tricky. If I want this class to have pointer-value semantics, we need to be able to convert between different types! That is, if I have:
#include <iostream>
#include <string>
#include "value_ptr.hpp"
struct Animal {
virtual bool isAlive() { return true; }
virtual std::string speak() = 0;
};
struct Dog : public Animal {
virtual std::string speak() { return "woof!"; }
};
struct Cat : public Animal {
virtual std::string speak() { return "meow!"; }
};
I want this to work:
value_ptr<Cat> cat;
value_ptr<Animal> animal = cat;
std::cout << animal->speak() << std::endl; // prints "meow"
Let’s see what errors we get at first:
./value_ptr.hpp:25:29: error: allocating an object of abstract class type
'Animal'
pointedAt = new T(std::forward<Args>(args)...);
^
test.cpp:23:28: note: in instantiation of function template specialization
'nmh::value_ptr<Animal>::value_ptr<nmh::value_ptr<Cat> &>' requested here
value_ptr<Animal> an = cat;
^
test.cpp:9:25: note: unimplemented pure virtual method 'speak' in 'Animal'
virtual std::string speak() = 0;
^
1 error generated.
That’s pretty nasty.
Alright, let’s see what we can do!
First, let’s create a new copy constructor, which takes a reference to a value_ptr
of any subclass of the contianed class:
template<typename O>
value_ptr(value_ptr<O>& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = new O(*o.pointedAt);
}
We use enable_if
here to ensure that we only work for sublcasses of T.
I’m fairly certain this isn’t neccisary as we assign pointedAt (type T*
) to a pointer of type O*
, which only works if T
is a base of O
, but we’ll leave it in.
Now, pointed_at
is private, so we’ll also need to make these classes friends:
class value_ptr {
template<typename O>
friend class value_ptr;
}
Did you know C++ friends could be templates? I certainly didn’t!
Defining an equals operator for this is actually pretty damn tricky.
The problem comes from enable_if
.
operator=
doesn’t take two parameters.
What are we to do?
Well, for some odd reason, it turns out that… we don’t need to? It’s actually somehow working already! I can run this test program just fine:
#include <iostream>
#include <string>
#include "value_ptr.hpp"
using nmh::value_ptr;
struct Bar{};
struct Animal {
virtual bool isAlive() { return true; }
virtual std::string speak() = 0;
virtual ~Animal() {}
};
struct Dog : public Animal {
virtual std::string speak() { return "woof!"; }
};
struct Cat : public Animal {
virtual std::string speak() { return "meow!"; }
};
int main() {
value_ptr<Cat> cat;
value_ptr<Animal> an(cat);
value_ptr<Dog> dog;
std::cout << an->speak() << std::endl;
an = dog;
std::cout << an->speak() << std::endl;
}
I did some research into this, and this is what’s happening:
- The compiler tries to find
operator=
that fits typevalue_ptr<Dog>
and failes - The compiler now tries to find a way to turn
value_ptr<Dog>
into something we can actually use - There’s a single-argument constructor for a
value_ptr<Animal>
, which is not marked explicit, and we have anoperator=
overload forvalue_ptr<Animal>
- The compiler calls this, then calls
operator=(value_ptr<Animal>)
This isn’t the behavior we want. It makes a temporary object needlessly. So, let’s fix it by creating new assignment operators:
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(const value_ptr<O>& other) {
std::cout << "template" << std::endl;
*pointedAt = *other.pointedAt;
return *this;
}
4. Fixing the Slicing Problem
It’s at about this time that I realized I had a huge mistake in my code: it slices! More specifically, the line:
*pointedAt = *other.pointedAt;
Will actually make a copy of the animal. Gross! Let’s make it non-slicing by allocating a new object, and deleting the old one:
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(const value_ptr<O>& other) {
if(pointedAt != nullptr) delete pointedAt;
pointedAt = new O(*other.pointedAt);
return *this;
}
Awesome!
5. Move assignemnt
We want to be able to move-assign the class as well, to avoid allocating things needlessly.
Let’s do that!
It’s not too hard, we basically make two copies of the operator=
we already defined that just swap the pointers.
Very simple:
value_ptr<T>& operator=(value_ptr<T>&& other) {
if(pointedAt != nullptr) delete pointedAt;
pointedAt = other.pointedAt;
other.pointedAt = nullptr;
return *this;
}
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(value_ptr<O>&& other) {
if(pointedAt != nullptr) delete pointedAt;
pointedAt = other.pointedAt;
other.pointedAt = nullptr;
return *this;
}
While we’re at it, let’s define a move constructor that takes subclass value_ptr
as well:
template<typename O>
value_ptr(value_ptr<O>&& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = o.pointedAt;
o.pointedAt = nullptr;
}
6. Deletors
This is where things get tricky.
In C++, if your class has virtual members, your destructor pretty much needs to be virtual. Seriously, if it’s not, some very nasty things will happen. If you have code like this:
struct Animal {
virtual std::string speak() = 0;
};
struct Cat : public Animal {
struct PurHelper {
size_t purrCount;
}
PurHelper *helper;
Cat() {
helper = new PurHelper();
helper->purrCount = 0;
}
virtual std::string speak() {
return "meow!";
}
~Cat() {
delete helper;
}
}
int main() {
Animal *an = new Cat();
delete an;
}
The PurHelper
object will leak, because delete an
calls Animal::~Animal()
, which is not virutal.
This means that Cat::~Cat()
will never get called!
Interestingly, however, this code doesn’t have that problem:
int main() {
std::shared_ptr<Animal> an = std::make_shared<Cat>();
}
Cat::~Cat()
will be called in this case!
The reason for this is that shared_ptr
stores a special deleter
object.
This is basically an object that knows how to delete the pointed-to object when its lifetime ends.
Interestingly, std::unique_ptr
doesn’t share this behavior.
If you use this function:
int main() {
std::unique_ptr<Animal> an = std::make_unique<Cat>();
}
~Cat()
will never be called.
Why?
Well, the logic is that unique_ptr
should have almost no runtime cost.
It does store a deleter, but as part of the type itself.
shared_ptr
allocates this deleter on the heap, so when you assign shared_ptr<Animal>
to a shared_ptr<Cat>
, the pointer to the deleter gets stored as well.
Meanwhile, the unique_ptr
stores the deleter as part of the type.
So, when you assign a unique_ptr<Animal>
to a unique_ptr<Cat>
, it will actually call the deleter for unique_ptr<Animal>
, which in this case uses operator delete
.
Now we have a decision to make: Do we want to provide shared_ptr
-esque behavior, or unique-ptr
-esque behavior?
Well, in this case, I think I have to go with shared_ptr
-esque behavior.
This also gives us the chance to implement custom deleters, which is always great!
- Deleters
In order to transparently support custom deleters, we’re going to have to use type erasure.
This is needed if we want to be able to use value_ptr<Cat>
with an arbitrary deleter.
Sadly, this also means we’re going to have to allow deleters to be passed as arguments, so our in-place forwarding constructor is going to no longer work—value_ptr
is going to have to be construced with a pointer.
Let’s first write the structure that type erases deleting for us.
This is a bit trick to do.
Using our Animal
and Cat
examples from before, we want the following behavior:
value_ptr<Cat> cat (new Cat()); // Makes a `value_ptr<Cat>` with `std::default_delete<Cat>` as the deleter
value_ptr<Animal> an = cat; // copies the `cat` into a new pointer, and copies the *deleter* into a new pointer
It’s easy to see why this is tricky. The trivial implementation, without polymorphism, is easy:
class Deleter {
public:
virtual void delete(T *in) = 0;
}
template<typename _Deletor>
class DeleterImpl : public Deleter {
public:
_Deleter del;
DeleterImpl(const _Deletor& d) : del(d) {}
virtual void delete(T *in) {
del(in);
}
};
The problem comes from the signature of the virtual function.
T = Animal
in value_ptr<Animal>
, but T = Cat
in value_ptr<Cat>
.
How can we get around this?
Well, let me show you the (horrific) solution, then explain why it works.
First up, brace yourself:
namespace nmh {
template<typename T>
class value_ptr {
protected:
T* pointedAt;
template<typename O>
friend class value_ptr;
struct value_deleter {
virtual void _delete(T* in) = 0;
virtual value_deleter* _clone() = 0;
virtual ~value_deleter() {}
};
value_deleter *deleter;
template<typename Del = std::default_delete<T>>
struct value_deleter_impl : public value_deleter {
Del del;
value_deleter_impl(const Del& in) : del(in) {}
value_deleter_impl(const value_deleter_impl<Del>&) = default;
virtual void _delete(T* in) override {
del(in);
}
virtual value_deleter* _clone() override {
return new value_deleter_impl<Del>(*this);
}
};
template<typename Other,
typename = std::enable_if_t<std::is_base_of_v<Other, T>>>
struct converting_deleter : public value_ptr<Other>::value_deleter {
value_deleter* del;
converting_deleter(value_deleter* _del) : del(_del) {}
virtual void _delete(Other* in) override {
// by the contract of converting_deleter, this is legitimate:
del->_delete(static_cast<T*>(in));
}
virtual typename value_ptr<Other>::value_deleter* _clone() override {
return new converting_deleter(del->_clone());
}
virtual ~converting_deleter() {
delete del;
}
};
Let’s break this down, shall we?
First, we have value_ptr<T>::value_deleter
.
This is an abstract type that allows us to perform type erasure.
It can clone itself (needed to provide value semantics upon copying) and it can delete an object of type T
you pass in.
Next, we have the specialization of it, value_ptr<T>::value_deleter_impl<Del>
.
This is a subclass of value_deleter
that stores an actual deleter.
This deleter’s type is templated as Del
, so it can be any input type—lambdas, callable objects, whatever!
When _delete(T*)
is called, it uses the deleter on the input pointer, in order to delete it.
_clone
returns a clone of the object.
Next, the horrible part: value_ptr<T>::converting_deleter<O>
.
This doesn’t subclass value_ptr<T>::value_deleter
, but instead value_ptr<O>::value_deleter
.
It’s a deleter that works with a value_ptr
that stores type O
, where O
is any superclass of T
!
Internally, it stores a value_ptr<T>::value_deleter*
, then casts its input pointer (of type O*
) to delegate to the actual deleter!
Essentially, it’s a wrapper that tells a value_ptr<T>
how to delete an object of type O
.
The rest of the class basically required re-writing for this to work. Let’s go over each new member, in order, to see what’s changed:
value_ptr(value_ptr<T> const & other) {
pointedAt = new T(*other.pointedAt);
deleter = other->deleter._clone();
}
This copies the object that other
points at, and also other
’s deleter.
template<typename O>
value_ptr(const value_ptr<O>& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = new O(*o.pointedAt);
deleter = new typename value_ptr<O>::template converting_deleter<T>(o.deleter->_clone());
}
template<typename O>
value_ptr(value_ptr<O>& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = new O(*o.pointedAt);
deleter = new typename value_ptr<O>::template converting_deleter<T>(o.deleter->_clone());
}
This now takes a value_ptr<O>
, copies it’s value, and clones its deleter into a converting_deleter
so our deleter can use it.
The template
is in the place it is so the compiler can parse this weird nested template hell correctly.
This is pretty simple otherwise.
Slightly harder is this:
template<typename O>
value_ptr(value_ptr<O>&& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = o.pointedAt;
deleter = new typename value_ptr<O>::template converting_deleter<T>(o.deleter);
o.pointedAt = nullptr;
}
If we move from another value_ptr, we can copy its pointedAt
over, as before.
However, we can also copy its deleter over.
We still need to wrap it into a converting_deleter
, but we don’t need to clone beforehand.
// note that assigning to a moved-from object is now undefined
// behavior, as it will dereference null
value_ptr(value_ptr<T>&& other) {
pointedAt = other.pointedAt;
deleter = other.deleter;
other.pointedAt = nullptr;
other.deleter = nullptr;
}
As before, we take over ownership of another value_ptr<T>
’s pointedAt
value if it’s passed to us after a move.
We do the same for its deleter.
Now, let’s get into the useful constructors, that actually take values.
value_ptr(T* _pointedAt) :
pointedAt(_pointedAt), deleter(new value_deleter_impl<std::default_delete<T>>(std::default_delete<T>()))
{}
In this case, we take over ownership of a given T
object the user has already allocated.
We use std::default_delete
as the deleter, which just calls operator delete
, a sensible default.
Let’s also allow the user to pass in custom deleter:
template<typename Del>
value_ptr(T* _pa, const Del& d) :
pointedAt(_pa),
deleter(new value_deleter_impl<Del>(d))
{}
Cool. Pretty simple and intiutive.
We modify operator=
in the exact same way as before, for all the overloads we have:
value_ptr<T>& operator=(const value_ptr<T>& other) {
*pointedAt = *other.pointedAt;
if(deleter != nullptr) delete deleter;
deleter = other.deleter;
return *this;
}
value_ptr<T>& operator=(value_ptr<T>&& other) {
if(pointedAt != nullptr) deletePointed();
if(deleter != nullptr) delete deleter;
pointedAt = other.pointedAt;
deleter = other.deleter;
other.pointedAt = nullptr;
other.deleter = nullptr;
return *this;
}
The deletePointed
function is new.
Essentially, it calls deleter->_delete(pointedAt);
.
In pervious iterations of value_ptr
, that line would have read delete pointedAt
, but now we need to use the custom deleter.
Here’s the other operator=
, for good measure:
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(const value_ptr<O>& other) {
if(pointedAt != nullptr) deletePointed();
pointedAt = new O(*other.pointedAt);
deleter = new typename value_ptr<O>::template converting_deleter<T>(other.deleter->_clone());
return *this;
}
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(value_ptr<O>&& other) {
if(pointedAt != nullptr) deletePointed();
if(deleter != nullptr) delete deleter;
pointedAt = other.pointedAt;
deleter = new typename value_ptr<O>::template converting_deleter<T>(other.deleter);
other.pointedAt = nullptr;
other.deleter = nullptr;
return *this;
}
Fairly simple once again.
Now, our destructor changes a tiny bit:
~value_ptr() {
if(pointedAt == nullptr) {
// nothing for now!
}
else {
deletePointed();
}
if(deleter != nullptr) {
delete deleter;
}
}
All we really do is remember to delete our deleter. Pretty simple.
8. Yet more Errors: operator=
and copying.
After sleeping on this, I realized that I had yet another error: copying values doesn’t neccisarily work correctly. Let’s have this example:
struct Animal {
virtual std::string speak() = 0;
};
struct Dog : public Animal {
bool isGood = true;
virtual std::string speak() override {
return good ? "Ruff!" : "Growl!";
}
};
struct Cat: public Animal {
virtual std::string speak() override {
return "purr";
}
}
value_ptr<Animal> broken(value_ptr<Animal> in) {
value_ptr<Animal> swap(new Cat());
swap = in;
return swap;
}
int main() {
value_ptr<Dog> broke(new Dog());
auto i = broken(broke);
}
This will eventually call *pointedAt = *in.pointedAt
in value_ptr
.
This is bad, because pointedAt
is a pointer to type Cat
, but the object being assigned has type Dog
.
To fix this, we need to type-erase the assignment/copying operator as well.
Let’s go and do that:
namespace nmh {
template<typename T>
class value_ptr {
protected:
T* pointedAt;
template<typename O>
friend class value_ptr;
struct value_deleter {
virtual void _delete(T* in) = 0;
virtual value_deleter* _clone() = 0;
virtual T* clonePointed(T*) = 0;
virtual ~value_deleter() {}
};
value_deleter *deleter;
template<typename Del = std::default_delete<T>>
struct value_deleter_impl : public value_deleter {
Del del;
value_deleter_impl(const Del& in) : del(in) {}
value_deleter_impl(const value_deleter_impl<Del>&) = default;
virtual void _delete(T* in) override {
if(in == nullptr) {
throw std::runtime_error("Deleting nullptr");
}
del(in);
}
virtual T* clonePointed(T* in) override {
return new T(*in);
}
virtual value_deleter* _clone() override {
return new value_deleter_impl<Del>(*this);
}
};
template<typename Other,
typename = std::enable_if_t<std::is_base_of_v<Other, T>>>
struct converting_deleter : public value_ptr<Other>::value_deleter {
value_deleter* del;
converting_deleter(value_deleter* _del) : del(_del) {}
virtual void _delete(Other* in) override {
// by the contract of converting_deleter, this is legitimate:
del->_delete(static_cast<T*>(in));
}
virtual typename value_ptr<Other>::value_deleter* _clone() override {
return new converting_deleter(del->_clone());
}
virtual Other* clonePointed(Other *in) override {
return del->clonePointed(static_cast<T*>(in));
}
virtual ~converting_deleter() {
delete del;
}
};
public:
value_ptr(value_ptr<T> const & other) {
pointedAt = other.rawClone();
deleter = other->deleter._clone();
}
value_ptr(value_ptr<T>& other) {
pointedAt = other.rawClone();
deleter = other.deleter->_clone();
}
template<typename O>
value_ptr(const value_ptr<O>& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = new O(*o.pointedAt);
deleter = new typename value_ptr<O>::template converting_deleter<T>(o.deleter->_clone());
}
template<typename O>
value_ptr(value_ptr<O>& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = new O(*o.pointedAt);
deleter = new typename value_ptr<O>::template converting_deleter<T>(o.deleter->_clone());
}
template<typename O>
value_ptr(value_ptr<O>&& o,
typename std::enable_if_t<std::is_base_of_v<T, O>,
int> v = 0) {
pointedAt = o.pointedAt;
deleter = new typename value_ptr<O>::template converting_deleter<T>(o.deleter);
o.pointedAt = nullptr;
o.deleter = nullptr;
}
// note that assigning to a moved-from object is now undefined
// behavior, as it will dereference null
value_ptr(value_ptr<T>&& other) {
pointedAt = other.pointedAt;
deleter = other.deleter;
other.pointedAt = nullptr;
other.deleter = nullptr;
}
value_ptr(T* _pointedAt) :
pointedAt(_pointedAt), deleter(new value_deleter_impl<std::default_delete<T>>(std::default_delete<T>()))
{}
template<typename Del>
value_ptr(T* _pa, const Del& d) :
pointedAt(_pa),
deleter(new value_deleter_impl<Del>(d))
{}
value_ptr<T>& operator=(const value_ptr<T>& other) {
if(pointedAt != nullptr) {
deletePointed();
}
pointedAt = other.rawClone();
if(deleter != nullptr) delete deleter;
deleter = other.deleter;
return *this;
}
value_ptr<T>& operator=(value_ptr<T>&& other) {
if(pointedAt != nullptr) deletePointed();
if(deleter != nullptr) delete deleter;
pointedAt = other.pointedAt;
deleter = other.deleter;
other.pointedAt = nullptr;
other.deleter = nullptr;
return *this;
}
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(const value_ptr<O>& other) {
if(pointedAt != nullptr) deletePointed();
pointedAt = other.rawClone();
deleter = new typename value_ptr<O>::template converting_deleter<T>(other.deleter->_clone());
return *this;
}
template<typename O>
typename std::enable_if_t<std::is_base_of_v<T, O>,
value_ptr<T>&>
operator=(value_ptr<O>&& other) {
if(pointedAt != nullptr) deletePointed();
if(deleter != nullptr) delete deleter;
pointedAt = other.pointedAt;
deleter = new typename value_ptr<O>::template converting_deleter<T>(other.deleter);
other.pointedAt = nullptr;
other.deleter = nullptr;
return *this;
}
T* operator->() {
return pointedAt;
}
~value_ptr() {
if(pointedAt == nullptr) {
// nothing for now!
}
else {
deletePointed();
}
if(deleter != nullptr) {
delete deleter;
}
}
private:
void deletePointed() {
if(deleter == nullptr) {
throw std::runtime_error("Probably impossible?");
}
deleter->_delete(pointedAt);
}
T* rawClone() {
return deleter->clonePointed(pointedAt);
}
};
}
We can thankfully move the cloning into the deleter. This, of course, completely ruins its name, since it now does more than delete things, but we’re going to stay too lazy to do anything to it for now.
9. Niceties
Okay, we have a working implementation.
Let’s add some convenience functions to help make this a bit easier.
First, let’s add a swap.
If we add a swap function to our namespace, then std::swap
will work, so let’s do that…
template<typename T>
void swap(value_ptr<T>& a, value_ptr<T>& b) {
auto p = a.pointedAt;
auto d = a.deleter;
a.pointedAt = b.pointedAt;
a.deleter = b.deleter;
b.pointedAt = p;
b.deleter = d;
}
10. Conclusions
Writing this (seemingly simple) class was a lot harder than I thought. I went through several iterations, all of which had their own issues. I’m still not 100% certain that this works exactly as it should!
This goes to show two things, I think. The first is that writing manual memory-management code in C++ is hard. It’s full of weird edge cases, and there’s a bunch of things to think about. If you’re not careful, things will explode in your face.
The second is that C++ is powerful.
The idea of a value_ptr
is pretty much unheard of in other languages, yet everything works out nicely here.
We used quite a lot of C++’s template features, and the end result is a nice, useable library that provides value pointer semantics!
I’ve been writing a lot more C++ lately, and I’ve been really enjoying it.
For all its flaws, it’s exteremly powerful, and with shared_ptr
and unique_ptr
it’s pretty easy to not shoot yourself in the foot.
I’ve written before about how much I love Ruby for putting a lot of power in the hands of the programmer with its metaprogramming capability.
I like C++ for much the same reasons—especially since C++ gives you metaprogramming power along with speed and type-safety, neither of which Ruby has.
You can find the full code here.
I’m more than happy to accept changes to fix any bugs I might have missed.
If there’s any sort of demmand for a value_ptr
as a standalone library, I’ll release one (and write more tests to verify things actually work).