This article examines constructors that are often overlooked. It examines their use cases and explains why the added functionality is meaningful in relation to smart pointers.
Default Constructor
Most people remember the default constructor (a zero-argument constructor), but it is sometimes overlooked.
The default constructor is useful when the type is used in a context where objects of the type need to be instantiated dynamically by another library (an example is a container resized; when a container is made larger by a resize, new members will need to be constructed, it is the default constructor that will provide these extra instances).
The default constructor is usually very trivial and thus worth the investment.
Smart Pointer Default Constructor
namespace ThorsAnvil
{
template<typename T>
class UP
{
T* data;
public:
UP()
: data(nullptr)
{}
.....
};
}
The nullptr
In C++11, the nullptr
was introduced to replace the old, broken NULL
and/or the even more broken 0
in contexts where you want a pointer that points at nothing. The nullptr
is automatically converted to any pointer type or a boolean, but it fixed the previous bug (or bad feature) and will not convert to a numeric type.
nullptr Usage Example
#include <string>
int main()
{
char* tmp = nullptr;
std::string* str = nullptr;
bool tst = nullptr;
int val = nullptr;
int val = NULL;
}
The nullptr
provides some opportunities to make the code shorter/cleaner when initializing smart pointers to be empty. Because we are using explicit one-argument constructors, the compiler can not automatically convert a nullptr
into a smart pointer; it must be done explicitly by the developer.
nullptr failing on Smart Pointer
void workWithSP(ThorsAnvil::UP<int>&& sp)
{ }
int main()
{
workWithSP(nullptr);
workWithSP(ThorsAnvil::UP<int>(nullptr));
}
This is overly verbose. There is no danger involved in automatically forming a smart pointer around a nullptr
. Because nullptr
has its own type, std::nullptr_t,
we can add a constructor to explicitly simplify this case, making it easier to read.
Smart Pointer with std::nullptr_t constructor
namespace ThorsAnvil
{
template<typename T>
class UP
{
public:
UP(std::nullptr_t)
: data(nullptr)
{}
....
};
}
void workWithSP(ThorsAnvil::UP<int>&& sp)
{ }
int main()
{
workWithSP(nullptr);
ThorsAnvil::UP<int> data = nullptr;
data = nullptr;
}
Move Semantics
Move semantics were introduced with C++ 11. So though we can not copy the ThorsAnvil::UP
object, it should be movable. The compiler will generate a default move constructor for a class under certain situations, but because we have defined a destructor for ThorsAnvil::UP
, we must manually define the move constructor.
Move semantics say that the source object may be left in an undefined (but must be valid) state. So, the easiest way to implement this is to swap the state of the current object with the source object (we know our state is valid, so just swap it with the incoming object state). Its destructor will then take care of destroying the pointer we are holding.
Smart Pointer Move Semantics
namespace ThorsAnvil
{
template<typename T>
class UP
{
T* data;
public:
void swap(UP& src) noexcept
{
std::swap(data, src.data);
}
UP(UP&& moving) noexcept
{
moving.swap(*this);
}
UP& operator=(UP&& moving) noexcept
{
moving.swap(*this);
return *this;
}
.....
};
template<typename T>
void swap(UP<T>& lhs, UP<T>& rhs)
{
lhs.swap(rhs);
}
}
Derived Type Assignment.
Assigning derived class pointers to a base class pointer object is quite a common feature in C++.
Derived Example
class Base
{
public:
virtual ~Base() {}
virtual void doAction() = 0;
};
class Derived1: public Base
{
public:
virtual void doAction() override;
};
class Derived2: public Base
{
public:
virtual void doAction() override;
};
int main(int argc, char* argv[])
{
Derived1* action1 = new Derived1;
Derived2* action2 = new Derived2;
Base* action = (argc == 2) ? action1 : action2;
action->doAction();
}
If we try the same code with our current constructors, we will get compile errors.
Derived Example with Smart Pointers
int main(int argc, char* argv[])
{
ThorsAnvil::UP<Derived1> action1 = new Derived1;
ThorsAnvil::UP<Derived2> action2 = new Derived2;
ThorsAnvil::UP<Base> action = std::move((argc == 2) ? action1 : action2);
action->doAction();
}
This is because C++ considers ThorsAnvil::UP<Derived1>
, ThorsAnvil::UP<Derived2>
and ThorsAnvil::UP<Base>
to be three distinct unrelated classes. As this kind of pointer usage is inherent in how C++ is used, the smart pointer must be designed for this use case.
To solve this, we need to allow different types of smart pointers to be constructed from other types of smart pointers, but only where the enclosed types are related.
Derived Smart Pointer transfer
namespace ThorsAnvil
{
template<typename T>
class UP
{
T* data;
public:
T* release()
{
T* tmp = nullptr;
std::swap(tmp, data);
return tmp;
}
template<typename U>
UP(UP<U>&& moving)
{
UP<T> tmp(moving.release());
tmp.swap(*this);
}
template<typename U>
UP& operator=(UP<U>&& moving)
{
UP<T> tmp(moving.release());
tmp.swap(*this);
return *this;
}
.....
};
}
Updated Unique Pointer
Combining the constructor/assignment operators discussed in this article with the ThorsAnvil::UP
that we defined in the first article in the series: Unique Pointer, we obtain the following:
ThorsAnvil::UP Version 3
namespace ThorsAnvil
{
template<typename T>
class UP
{
T* data;
public:
UP()
: data(nullptr)
{}
explicit UP(T* data)
: data(data)
{}
~UP()
{
delete data;
}
UP(std::nullptr_t)
: data(nullptr)
{}
UP& operator=(std::nullptr_t)
{
reset();
return *this;
}
UP(UP&& moving) noexcept
{
moving.swap(*this);
}
UP& operator=(UP&& moving) noexcept
{
moving.swap(*this);
return *this;
}
template<typename U>
UP(UP<U>&& moving)
{
UP<T> tmp(moving.release());
tmp.swap(*this);
}
template<typename U>
UP& operator=(UP<U>&& moving)
{
UP<T> tmp(moving.release());
tmp.swap(*this);
return *this;
}
UP(UP const&) = delete;
UP& operator=(UP const&) = delete;
T* operator->() const {return data;}
T& operator*() const {return *data;}
T* get() const {return data;}
explicit operator bool() const {return data;}
T* release() noexcept
{
T* result = nullptr;
std::swap(result, data);
return result;
}
void swap(UP& src) noexcept
{
std::swap(data, src.data);
}
void reset()
{
T* tmp = releae();
delete tmp;
}
};
template<typename T>
void swap(UP<T>& lhs, UP<T>& rhs)
{
lhs.swap(rhs);
}
}
Summary
In the last two articles (Unique Pointer and Shared Pointer) we covered some basic mistakes that I have often seen developers make when attempting to create their own smart pointer. I also introduce four important C++ concepts:
In this article, I focused on several constructors/assignment operators that can be overlooked.