Smart Pointers
A common shortcoming of C++ is that programmers make mistakes when they allocate memory, often failing to ensure proper de-allocation.
For example, consider the case of a ‘normal’ pointer:
//
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Creating Resource..." << std::endl;
}
~Resource()
{
std::cout << "Deleting Resource..." << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
Resource* ptr = new Resource();
// do stuff with ptr here
delete ptr;
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}which outputs:
Creating Resource...
Deleting Resource...
Program ended with exit code: 0
So far, so good.
But what happens if myFunction doesn’t terminate normally?
//
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Creating Resource..." << std::endl;
}
~Resource()
{
std::cout << "Deleting Resource..." << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
Resource* ptr = new Resource();
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; // the function returns early, and ptr won’t be deleted!
// do stuff with ptr here
delete ptr;
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}This program outputs:
Creating Resource...
Enter an integer: Program ended with exit code: 0
where the user entered End-of-file (Ctrl/D) in response to the prompt.
See the problem?
Or how about if there’s an exception?
//
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Creating Resource..." << std::endl;
}
~Resource()
{
std::cout << "Deleting Resource..." << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
Resource* ptr = new Resource();
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
throw 0; // the function returns early, and ptr won’t be deleted!
// do stuff with ptr here
delete ptr;
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}This program outputs:
Creating Resource...
Enter an integer: libc++abi: terminating due to uncaught exception of type int
where the user entered End-of-file (Ctrl/D) in response to the prompt.
See the problem here, too?
How Do We Solve This?
Remember that a class can contain a destructor which gets automatically executed whenever a instance of that class goes out of scope.
So, how about we create a class as a kind of “Smart Pointer”?
//
// SmartPtr.h
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#ifndef SmartPtr_h
#define SmartPtr_h
#include <iostream>
template <typename T>
class SmartPtr
{
public:
// Pass in a pointer to "own" via the constructor
SmartPtr(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// The destructor will make sure it gets deallocated
~SmartPtr()
{
delete m_ptr;
}
// Overload dereference and operator-> so we can use Auto_ptr1 like m_ptr.
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
private:
T* m_ptr {}; // The encapsulated pointer
};
#endif /* SmartPtr_h */
//
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
#include "SmartPtr.h"
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Resource acquired..." << std::endl;
}
~Resource()
{
std::cout << "Resource destroyed..." << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
SmartPtr<Resource> resource( new Resource() ); /* not <Resource> */
/* No need for delete */
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}We’ve created a SmartPtr class to accomplish this.
It’s a template class so we can use it generally, for any kind of resource.
It’s also declared dereference and operator-> operators, so that we can use an instance of SmartPtr like a regular pointer.
This program outputs:
Resource acquired...
Resource destroyed...
Program ended with exit code: 0
Does this work when the function returns prematurely?
//
// SmartPtr.h
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#ifndef SmartPtr_h
#define SmartPtr_h
#include <iostream>
template <typename T>
class SmartPtr
{
public:
// Pass in a pointer to "own" via the constructor
SmartPtr(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// The destructor will make sure it gets deallocated
~SmartPtr()
{
delete m_ptr;
}
// Overload dereference and operator-> so we can use Auto_ptr1 like m_ptr.
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
private:
T* m_ptr {}; // The encapsulated pointer
};
#endif /* SmartPtr_h */
//
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
#include "SmartPtr.h"
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Resource acquired..." << std::endl;
}
~Resource()
{
std::cout << "Resource destroyed..." << std::endl;
}
void print()
{
std::cout << "I'm here! " << m_data << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
SmartPtr<Resource> resource( new Resource() ); /* not <Resource> */
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; // the function returns early
// do stuff
resource->print();
/* No need for delete */
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}This program outputs:
Resource acquired...
Enter an integer: Resource destroyed...
Program ended with exit code: 0
where the user entered End-of-file (Ctrl/D) in response to the prompt.
It worked!
Are we home free?
Let’s see…
//
// SmartPtr.h
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#ifndef SmartPtr_h
#define SmartPtr_h
#include <iostream>
template <typename T>
class SmartPtr
{
public:
// Pass in a pointer to "own" via the constructor
SmartPtr(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// The destructor will make sure it gets deallocated
~SmartPtr()
{
delete m_ptr;
}
// Overload dereference and operator-> so we can use Auto_ptr1 like m_ptr.
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
private:
T* m_ptr {}; // The encapsulated pointer
};
#endif /* SmartPtr_h */
//
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
#include "SmartPtr.h"
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Resource acquired..." << std::endl;
}
~Resource()
{
std::cout << "Resource destroyed..." << std::endl;
}
void print()
{
std::cout << "I'm here! " << m_data << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
SmartPtr<Resource> resource( new Resource() ); /* not <Resource> */
SmartPtr<Resource> resource2(resource);
SmartPtr<Resource> resource3 = resource2;
/* No need for delete */
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}This program now outputs:
Resource acquired...
Resource destroyed...
Resource destroyed...
Smart Pointers(65008,0x7ff84e4069c0) malloc: *** error for object 0x600000208fe0: pointer being freed was not allocated
Smart Pointers(65008,0x7ff84e4069c0) malloc: *** set a breakpoint in malloc_error_break to debug
Oh-oh! What’s the problem?
The problem arises because we forgot that C++ will automatically provide a copy constructor and an assignment operator if we don’t. And the ones it creates do a shallow copy. Hence, the problem seen above.
How To Solve This Problem?
We could explicitly create the copy constructor and assignment operator to be private, thus disabling them:
//
// SmartPtr.h
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#ifndef SmartPtr_h
#define SmartPtr_h
#include <iostream>
template <typename T>
class SmartPtr
{
public:
// Pass in a pointer to "own" via the constructor
SmartPtr(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// The destructor will make sure it gets deallocated
~SmartPtr()
{
delete m_ptr;
}
// Overload dereference and operator-> so we can use Auto_ptr1 like m_ptr.
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
private:
SmartPtr(const SmartPtr& other); // Copy Constructor
SmartPtr& operator=(const SmartPtr& other); // Assignment operator
T* m_ptr {}; // The encapsulated pointer
};
#endif /* SmartPtr_h *///
// main.cpp
// Smart Pointers
//
// Created by Bryan Higgs on 4/4/25.
//
#include <iostream>
#include "SmartPtr.h"
class Resource
{
public:
Resource()
: m_data("Data")
{
std::cout << "Resource acquired..." << std::endl;
}
~Resource()
{
std::cout << "Resource destroyed..." << std::endl;
}
void print()
{
std::cout << "I'm here! " << m_data << std::endl;
}
private:
std::string m_data;
};
void myFunction()
{
SmartPtr<Resource> resource( new Resource() ); /* not <Resource> */
SmartPtr<Resource> resource2(resource);
SmartPtr<Resource> resource3 = resource2;
/* No need for delete */
}
int main(int argc, const char * argv[])
{
myFunction();
return 0;
}This program now fails to compile:
main.cpp:37:22 Calling a private constructor of class 'SmartPtr<Resource>'
main.cpp:38:34 Calling a private constructor of class 'SmartPtr<Resource>'
What if, instead of having our copy constructor and assignment operator copy the pointer (“copy semantics”), we instead transfer/move ownership of the pointer from the source to the destination object?
Move Semantics
Move semantics is a set of semantic rules and tools of the C++ language. It was designed to move objects, whose lifetime expires, instead of copying them. The data is transferred from one object to another. In most cases, the data transfer does not move this data physically in memory. It helps to avoid expensive copying.
Move semantics was introduced in the C++11 standard. To implement it, rvalue references, move constructors, and the move assignment operator were added. Also, some functions were added to the standard template library (STL) to support move semantics. For example, std::move and std::forward.
For example, if you were writing a Swap template function that accepts two objects of the same type, and swaps them. Let’s say you implemented this as follows:
template <typename T>
void Swap(T &lhs, T &rhs)
{
T t = lhs;
lhs = rhs;
rhs = t;
}However, what happens if you do the following:
std::vector<int> arrA(1'000'000, 0);
std::vector<int> arrB(1'000'000, 1);
Swap(arrA, arrB);This involves the std::vector copy constructor, which:
- Performs dynamic memory allocation for the specified number of elements
- Makes a deep copy of the elements from the passed std::vector
In other words, a lot of time-consuming and memory-consuming work.
Move semantics helps to remove the need to perform unnecessary copying. To use move semantics, we need to an rvalue reference, so we can tell the compiler to move the object.
[More to be supplied]…