Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Automatic Memory Management
Use RAII wrappers that automatically manage heap memory lifetime.
Introduction to smart pointers and move semantics
Memory leaks happen when dynamically allocated memory isn't properly cleaned up. Consider this function that manages a database connection:
void processData()
{
DatabaseConnection* conn{new DatabaseConnection()};
// process some data
delete conn;
}
While this looks straightforward, memory leaks can easily occur if the function exits before reaching the delete statement. This might happen through an early return:
#include <iostream>
void processData()
{
DatabaseConnection* conn{new DatabaseConnection()};
int errorCode{};
std::cout << "Enter error code: ";
std::cin >> errorCode;
if (errorCode == -1)
return; // function exits early, conn never gets deleted!
// process data
delete conn;
}
Or through an exception:
#include <iostream>
void processData()
{
DatabaseConnection* conn{new DatabaseConnection()};
int errorCode{};
std::cout << "Enter error code: ";
std::cin >> errorCode;
if (errorCode == -1)
throw std::runtime_error("Database error"); // function exits, conn leaked!
// process data
delete conn;
}
In both scenarios, the memory allocated for conn is leaked because delete never executes. Each time the function exits prematurely, more memory is lost. The fundamental problem is that raw pointers have no automatic cleanup mechanism.
Enter smart pointer classes
Classes provide destructors that automatically execute when an object goes out of scope. If we allocate memory in the constructor, we can deallocate it in the destructor, guaranteeing cleanup regardless of how the function terminates. This is the foundation of the RAII (Resource Acquisition Is Initialization) programming paradigm.
Can we use a class to manage our pointers? Absolutely!
Imagine a class designed specifically to hold and "own" a pointer, then automatically deallocate that pointer when the class object goes out of scope. As long as we create these objects as local variables, we're guaranteed proper cleanup no matter how our function terminates.
Here's an initial implementation:
#include <iostream>
template <typename T>
class SmartPtr1
{
T* m_ptr{};
public:
// Constructor takes ownership of the pointer
SmartPtr1(T* ptr=nullptr)
: m_ptr{ptr}
{
}
// Destructor ensures cleanup
~SmartPtr1()
{
delete m_ptr;
}
// Overload operators to make SmartPtr1 behave like a raw pointer
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
// Sample class to demonstrate functionality
class DatabaseConnection
{
public:
DatabaseConnection() { std::cout << "Database connection established\n"; }
~DatabaseConnection() { std::cout << "Database connection closed\n"; }
};
int main()
{
SmartPtr1<DatabaseConnection> conn{new DatabaseConnection()};
// ... use the connection
// No explicit delete needed
return 0;
} // conn goes out of scope here, automatically closing the connection
This program prints:
Database connection established
Database connection closed
Let's analyze how this works: We dynamically create a DatabaseConnection and pass it to our templated SmartPtr1 class. From that point, SmartPtr1 owns the DatabaseConnection (SmartPtr1 has a composition relationship with m_ptr). Since conn is a local variable with block scope, it will be destroyed when the block ends. When it's destroyed, the SmartPtr1 destructor automatically deletes the DatabaseConnection.
As long as SmartPtr1 is a local variable with automatic duration (hence the "Smart" part), the DatabaseConnection is guaranteed destruction when the block ends, regardless of how the function terminates.
Such a class is called a smart pointer. A smart pointer is a composition class designed to manage dynamically allocated memory and ensure proper deletion when the smart pointer goes out of scope. (Raw pointers are sometimes called "dumb pointers" because they lack automatic cleanup.)
Now let's revisit our processData() example using a smart pointer:
#include <iostream>
template <typename T>
class SmartPtr1
{
T* m_ptr{};
public:
SmartPtr1(T* ptr=nullptr)
: m_ptr{ptr}
{
}
~SmartPtr1()
{
delete m_ptr;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
class DatabaseConnection
{
public:
DatabaseConnection() { std::cout << "Database connection established\n"; }
~DatabaseConnection() { std::cout << "Database connection closed\n"; }
void query() { std::cout << "Executing query!\n"; }
};
void processData()
{
SmartPtr1<DatabaseConnection> conn{new DatabaseConnection()};
int errorCode{};
std::cout << "Enter error code: ";
std::cin >> errorCode;
if (errorCode == -1)
return; // function exits early, but cleanup still happens
// process data
conn->query();
}
int main()
{
processData();
return 0;
}
If the user enters a non-error value, the program prints:
Database connection established
Executing query!
Database connection closed
If the user enters -1 causing early termination, the program prints:
Database connection established
Database connection closed
Even with early termination, the DatabaseConnection is properly closed! Because conn is a local variable, it's destroyed when processData() terminates (regardless of how). The SmartPtr1 destructor ensures the DatabaseConnection is properly cleaned up.
A critical problem
The SmartPtr1 class has a critical flaw hidden in auto-generated code. Can you spot it before reading further?
(Hint: consider what member functions are auto-generated if you don't provide them)
Rather than explain, let's demonstrate:
#include <iostream>
template <typename T>
class SmartPtr1
{
T* m_ptr{};
public:
SmartPtr1(T* ptr=nullptr)
: m_ptr{ptr}
{
}
~SmartPtr1()
{
delete m_ptr;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
class DatabaseConnection
{
public:
DatabaseConnection() { std::cout << "Database connection established\n"; }
~DatabaseConnection() { std::cout << "Database connection closed\n"; }
};
int main()
{
SmartPtr1<DatabaseConnection> conn1{new DatabaseConnection()};
SmartPtr1<DatabaseConnection> conn2{conn1}; // Or: conn2 = conn1;
return 0;
} // conn1 and conn2 both go out of scope
This program prints:
Database connection established
Database connection closed
Database connection closed
Your program will likely crash here. See the problem? Without a custom copy constructor or assignment operator, C++ provides default versions that perform shallow copies. When we initialize conn2 with conn1, both smart pointers point to the same DatabaseConnection. When conn2 goes out of scope, it deletes the connection, leaving conn1 with a dangling pointer. When conn1 tries to delete its already-deleted connection, undefined behavior results (usually a crash).
A similar problem occurs when passing by value:
void useConnection(SmartPtr1<DatabaseConnection> conn)
{
}
int main()
{
SmartPtr1<DatabaseConnection> conn1{new DatabaseConnection()};
useConnection(conn1); // conn1 copied by value into parameter conn
return 0;
}
Here, conn1 is copied by value into parameter conn, so both hold the same address. When conn is destroyed at function end, conn1 is left dangling. When conn1 is later deleted, undefined behavior occurs.
How do we fix this?
We could explicitly delete the copy constructor and assignment operator, preventing copies entirely. But then how would we return a SmartPtr1 from a function?
??? createConnection()
{
DatabaseConnection* db{new DatabaseConnection()};
return SmartPtr1(db);
}
We can't return by reference (the local SmartPtr1 is destroyed, leaving a dangling reference). Returning the raw pointer db defeats the purpose of smart pointers (we might forget to delete it). Returning by value seems like the only option, but that creates shallow copies, duplicate pointers, and crashes.
We could make the copy constructor and assignment operator do deep copies, avoiding duplicate pointers. But copying can be expensive and may not be desirable or even possible. Plus, copying dumb pointers doesn't copy the pointed-to object, so why should smart pointers behave differently?
What's the solution?
Move semantics
What if instead of copying the pointer, we transfer/move ownership from source to destination? This is the core of move semantics: transferring ownership rather than making a copy.
Let's update SmartPtr1 to demonstrate:
#include <iostream>
template <typename T>
class SmartPtr2
{
T* m_ptr{};
public:
SmartPtr2(T* ptr=nullptr)
: m_ptr{ptr}
{
}
~SmartPtr2()
{
delete m_ptr;
}
// Copy constructor implementing move semantics
SmartPtr2(SmartPtr2& other) // note: not const
{
m_ptr = other.m_ptr; // transfer ownership
other.m_ptr = nullptr; // source no longer owns the pointer
}
// Assignment operator implementing move semantics
SmartPtr2& operator=(SmartPtr2& other) // note: not const
{
if (&other == this)
return *this;
delete m_ptr; // cleanup any existing resource
m_ptr = other.m_ptr; // transfer ownership
other.m_ptr = nullptr; // source no longer owns the pointer
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class DatabaseConnection
{
public:
DatabaseConnection() { std::cout << "Database connection established\n"; }
~DatabaseConnection() { std::cout << "Database connection closed\n"; }
};
int main()
{
SmartPtr2<DatabaseConnection> conn1{new DatabaseConnection()};
SmartPtr2<DatabaseConnection> conn2{}; // starts as nullptr
std::cout << "conn1 is " << (conn1.isNull() ? "null\n" : "not null\n");
std::cout << "conn2 is " << (conn2.isNull() ? "null\n" : "not null\n");
conn2 = conn1; // conn2 assumes ownership, conn1 becomes null
std::cout << "Ownership transferred\n";
std::cout << "conn1 is " << (conn1.isNull() ? "null\n" : "not null\n");
std::cout << "conn2 is " << (conn2.isNull() ? "null\n" : "not null\n");
return 0;
}
This program prints:
Database connection established
conn1 is not null
conn2 is null
Ownership transferred
conn1 is null
conn2 is not null
Database connection closed
Notice that operator= transferred ownership from conn1 to conn2! We avoid duplicate pointers and everything cleans up properly.
Note: Deleting a nullptr is safe and does nothing.
std::auto_ptr and why it was problematic
C++98 introduced std::auto_ptr, the first standardized smart pointer, implementing move semantics like our SmartPtr2. However, std::auto_ptr (and SmartPtr2) has serious problems:
First, implementing move semantics through copy constructors and assignment operators causes unexpected behavior. Passing std::auto_ptr by value transfers the resource to the function parameter, which is destroyed when the function ends. Accessing the original auto_ptr afterward dereferences a null pointer. Crash!
Second, std::auto_ptr always uses non-array delete, making it incompatible with dynamically allocated arrays (wrong deallocation type leads to memory leaks).
Third, std::auto_ptr doesn't work properly with standard library containers and algorithms, which expect copying to create actual copies, not perform moves.
Due to these shortcomings, std::auto_ptr was deprecated in C++11 and removed in C++17.
Moving forward
The fundamental problem with std::auto_ptr is that pre-C++11, C++ had no mechanism to differentiate "copy semantics" from "move semantics." Overriding copy semantics to implement move semantics leads to unexpected behavior. For example, conn1 = conn2 gives no indication whether conn2 will be modified.
In C++11, "move" was formally defined and move semantics were added to the language, properly distinguishing copying from moving. Throughout this chapter, we'll explore move semantics in detail and fix our SmartPtr2 class properly.
C++11 replaced std::auto_ptr with move-aware smart pointers: std::unique_ptr, std::weak_ptr, and std::shared_ptr. We'll explore the two most popular: std::unique_ptr (the direct auto_ptr replacement) and std::shared_ptr.
Summary
Memory leaks and raw pointers: Dynamically allocated memory can leak when functions exit early (via return or exception) before reaching delete statements. Raw pointers have no automatic cleanup mechanism.
Smart pointers concept: A smart pointer is a composition class that manages a dynamically allocated resource and ensures proper deletion when the smart pointer goes out of scope, following the RAII (Resource Acquisition Is Initialization) paradigm.
Basic implementation: Smart pointers use a destructor to automatically delete managed resources, and overload operator* and operator-> to behave like regular pointers. They should always be allocated on the stack, never dynamically.
The copy problem: Default copy constructors create shallow copies, causing multiple smart pointers to point to the same resource. When one is destroyed, it deletes the resource, leaving others with dangling pointers.
Move semantics solution: Instead of copying, move semantics transfer ownership from source to destination. The source pointer is set to nullptr after the move, preventing double deletion.
std::auto_ptr limitations: C++'s first standard smart pointer (C++98) implemented move semantics through copy constructors, causing unexpected behavior. It was deprecated in C++11 and removed in C++17 due to issues with standard library containers and array deletion.
Use C++11's move-aware smart pointers (std::unique_ptr, std::shared_ptr) instead of managing raw pointers yourself. Never use the deprecated std::auto_ptr.
Automatic Memory Management - Quiz
Test your understanding of the lesson.
Practice Exercises
Demonstrate Resource Cleanup with a Simple Smart Pointer
Create a simple Resource class that prints messages when constructed and destroyed. Use a basic IntHolder smart pointer wrapper to automatically manage the resource lifetime, demonstrating RAII principles.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!