Overview of Dynamic Memory and Smart Pointer
12/22/2021 Tags: C_C_plus_plusAbstract
The C++ language allows programmers to manually allocate/decallocate memory by static allocation or dynamic allocation. The static memory is allocated from the stack and used for local static objects, for class static data members and for variables defined outside any function. The dynamic memory is allocated from the free store or heap and its allocation with new and delete. However, dynamically allocated memory is notoriously tricky to manage correctly because it is surprisingly hard to ensure to either free memory at the right run1time, known as memory leak, or free the memory when there are still pointers referring to that memory. There is a way to avoid memory leak or manage dynamic allocation smartly by using smart pointer - unique_ptr. In this post, I would like to disscuss the basic concept of smart pointer std::unique_ptr, std::shared_ptr and std::weak_ptr.
Smart Pointers
Smart pointers is nice for when dynamic allocation is necessary. Since any dynamic memory requires a delete, it’s much easier, much safer, and much less error prone to let the smart pointer take care of the cleanup, rather than doing it manually. The C++ 11 library defines three kinds of smart pointers that differ in how they manage their underlying pointers:
std::unique_ptr class
The uses of smart pointer unique_ptr include providing exception safety for dynamically allocated memory, passing ownership of dynamically allocated memory to a function, and returning dynamically allocated memory from a function. The unique_ptr implementation could be referred from C++ STL library or boost C++ library.
CASE STUDY: unique_ptr.hpp in Boost C++ library
namespace movelib {
...
//! \tparam T Provides the type of the stored pointer.
//! \tparam D The deleter type:
//! - The default type for the template parameter D is default_delete. A client-supplied template argument D shall be a function object type, ...
...
template <class T, class D = default_delete<T> >
class unique_ptr
{
#if defined(BOOST_MOVE_DOXYGEN_INVOKED)
public:
unique_ptr(const unique_ptr&) = delete;
...
private:
...
public:
~unique_ptr()
{ if(m_data.m_p) m_data.deleter()(m_data.m_p); }
...
void swap(unique_ptr& u) BOOST_NOEXCEPT
{
::boost::adl_move_swap(m_data.m_p, u.m_data.m_p);
::boost::adl_move_swap(m_data.deleter(), u.m_data.deleter());
}
...
};
...
Note: Function template
- The format for declaring function template with type parameters is:
template <class identifier> function_declaration;
template <typename identifier> function_declaration; - A template parameter is a special kind of parameter that can be used to pass a type as argument.
Simple Examples
- Assigning static allocation into dynamic allocation (C++11)
- Using make_unique for initializing unique_ptr (C++14)
void Exam::SmartPointer() {
constexpr int SIZE = 8;
static const char chars[] = { 'B', 'e', 'S', 'm', 'a', 'r', 't', '!' };
std::unique_ptr<char[]> arr(new char[SIZE]);
for(int i = 0; i < SIZE; ++i) {
arr[i] = chars[i];
std::cout << arr[i] << "; ";
}
std::cout << std::endl;
}
struct Vec3
{
int x, y, z;
Vec3(): x(0), y(0), z(0) {}
Vec3(int x, int y, int z): x(x), y(y), z(z) {}
friend std::ostream& operator<<(std::ostream& os, Vec3& v) {
return os << '{' << "x:" << v.x << " y:" << v.y << " z:" << v.z << '}';
}
};
void Exam::SmartPointerMakeUnique() {
// Use the default constructor.
std::unique_ptr<Vec3> v1 = std::make_unique<Vec3>();
// Use the constructor that matches these arguments
std::unique_ptr<Vec3> v2 = std::make_unique<Vec3>(0, 1, 2);
// Create a unique_ptr to an array of 5 elements
std::unique_ptr<Vec3[]> v3 = std::make_unique<Vec3[]>(5);
std::cout << "V1 make_unique<Vec3>(): " << *v1 << '\n'
<< "V2 make_unique<Vec3>(0,1,2): " << *v2 << '\n'
<< "V3 make_unique<Vec3[]>(5): " << '\n';
for (int i = 0; i < 5; i++) {
std::cout << " " << v3[i] << '\n';
}
}
std::shared_ptr class
The smart pointer std::shared_ptr stored a pointer to a dynamically allocated object and the object pointed to is guaranteed to be deleted when the last shared_ptr pointing to it is destroyed or reset. In addition to reset the dynamical allocation, cycles of shared_ptr instances will not be reclaimed because the implementation uses reference counting. For example, if main() holds a shared_ptr to A, which directly or indirectly holds a shared_ptr back to A, A’s use count will be 2. Destruction of the original shared_ptr will leave A dangling with a use count of 1.
CASE STUDY: shared_ptr.hpp in Boost C++ library
// /usr/local/Cellar/boost/1.76.0/include/boost/interprocess/smart_ptr/shared_ptr.hpp
...
namespace boost{
namespace interprocess{
...
//!shared_ptr is parameterized on
//!T (the type of the object pointed to), VoidAllocator (the void allocator to be used
//!to allocate the auxiliary data) and Deleter (the deleter whose
//!operator() will be used to delete the object.
...
template<class T, class VoidAllocator, class Deleter>
class shared_ptr
{
#if !defined(BOOST_INTERPROCESS_DOXYGEN_INVOKED)
private:
typedef shared_ptr<T, VoidAllocator, Deleter> this_type;
#endif //#ifndef BOOST_INTERPROCESS_DOXYGEN_INVOKED
...
public:
//!Constructs an empty shared_ptr.
//!Use_count() == 0 && get()== 0.
shared_ptr()
: m_pn() // never throws
{}
...
//!Returns the number of shared_ptr objects, *this included,
//!that share ownership with *this, or an unspecified nonnegative
//!value when *this is empty.
...
long use_count() const // never throws
{ return m_pn.use_count(); }
...
}
...
Simple Example
Assigning value, pair value and object into dynamic allocation (C++11)
struct Base
{
Base() { std::cout << " Base::Base() \n" << std::endl;};
virtual ~Base() { std::cout << "Base::~Base()" << std::endl;};
};
struct Drived : public Base
{
Drived() { std::cout << " Drived::Drived() \n" << std::endl;};
virtual ~Drived() { std::cout << "Drived::~Drived()" << std::endl;};
};
void Exam::SmartPointerSharedPtr() {
std::shared_ptr<int> v1 = std::make_shared<int> (10);
std::shared_ptr< std::pair<int,int> > v2 = std::make_shared< std::pair<int, int> > (20, 30);
std::shared_ptr<Base> p = std::make_shared<Drived>();
std::cout << "std::make_shared<int> (10): " << *v1 << std::endl;
std::cout << "std::make_shared< std::pair<int, int> > (20, 30): "<< "(" << v2->first << ","<< v2->second << ")" << std::endl;
std::cout << "Drived object's use Count: " << p.use_count() << std::endl;
}
std::weak_ptr class
The smart pointer weak_ptr stores a “weak reference” to an object that’s already managed by shared_ptr. To access the object, a weak_ptr can be converted to a shared_ptr using the shared_ptr constructor or the member function lock.
By referring to stack overfolw question, we could know that the uses of smart pointer weak_ptr is a very good way to solve the dangling pointer problem. By just using raw pointers it is impossible to know if the referenced data has been deallovated or not.
// /usr/local/Cellar/boost/1.76.0/include/boost/interprocess/smart_ptr/weak_ptr.hpp
...
namespace boost{
namespace interprocess{
...
//!The class template is parameterized on T, the type of the object pointed to.
template<class T, class A, class D>
class weak_ptr
{
#if !defined(BOOST_INTERPROCESS_DOXYGEN_INVOKED)
private:
// Borland 5.5.1 specific workarounds
typedef weak_ptr<T, A, D> this_type;
...
public:
...
//!Effects: Constructs an empty weak_ptr.
//!Postconditions: use_count() == 0.
weak_ptr()
: m_pn() // never throws
{}
...
};
...
Simple Example
void Exam::SmartPointerWeakPtr() {
std::shared_ptr<int> sp1 (new int(10));
std::shared_ptr<int> sp2 (new int(20));
std::weak_ptr<int> wp1(sp1);
std::weak_ptr<int> wp2(sp2);
wp1.swap(wp2);
std::cout << "sp1 -> " << *sp1 << '\n';
std::cout << "sp2 -> " << *sp2 << '\n';
std::cout << "wp1 -> " << *wp1.lock() << '\n';
std::cout << "wp2 -> " << *wp2.lock() << '\n';
}
Reference
[1] Smart Pointers — unique_ptr, shared_ptr, weak_ptr
[3] cppreference: std::unique_ptr, std::shared_ptr, std::weak_ptr, std::make_unique
**Thanks for reading! Feel free to leave the comments below or email to me. Any pieces of advice or discussions are always welcome. :)**