In our previous lessons, we've primarily worked with stack memory. Stack memory is straightforward and efficient, automatically managing the lifecycle of our variables. However, it comes with limitations:
To overcome these limitations, C++ provides dynamic memory allocation using the heap. We'll dive deeper into dynamic memory in a dedicated chapter in the next course, but for now, let's see a brief overview.
Dynamic memory allows us to allocate memory at runtime, giving us more flexibility in managing our program's resources. However, this flexibility comes at a cost: dynamic memory management is error-prone. Common issues include:
To mitigate these issues, we can implement the design pattern of memory ownership. This concept suggests that resources (like objects stored in dynamically allocated memory) should have clear owners responsible for their lifecycle. We can imagine that certain objects or functions "own" other resources, managing their creation and destruction.
Smart pointers help implement this ownership model.
In a sense, they try to combine the best of both worlds. They give us the flexibility of dynamic memory allocation, with the simplicity of stack memory allocation where memory is automatically released when it is no longer needed.
The simplest form of smart pointer is a unique pointer, an implementation of which is available in the standard library as std::unique_ptr
. Like any pointer, a unique pointer points to an object in memory. The "unique" refers to the idea that it should be the only unique pointer that points to that object.
As such, we can imagine that the function or object that holds the unique pointer has exclusive ownership of the object that the pointer points to.
Unique pointers implement restrictions to help enforce this design. For example, we’ll see later in this lesson that it is difficult to create a copy of a unique pointer, as doing so would mean we now have two unique pointers pointing to the same object, contradicting the uniqueness.
make_unique()
By including <memory>
, we gain access to the std::make_unique()
function. Using this function is the preferred way of creating unique pointers:
#include <memory>
int main() {
auto Pointer { std::make_unique<int>(42) };
}
The <
and >
within this code indicates that make_unique
is a template function. We cover templates in detail in the next course.
For now, we should just note that we need to pass the type of data we want to create a pointer to within the <
and >
. In the previous example, we wanted to create a pointer to an int
, so we pass int
between the <
and >
Within the (
and )
, we can optionally pass any arguments along to the constructor of our data type. In previous example, we pass 42
, which will become the initial value of our int
.
The net effect of all this is that we have:
int
object allocated in the free store (dynamic memory)int
having the initial value of 42
std::unique_ptr
, which we’ve called Pointer
, within the stack frame of our main
function. That std::unique_ptr
is considered the sole owner of the integer that is stored in the heap.The return type of make_unique
is a std::unique_ptr
of the corresponding type. For example, std::make_unique<int>
will return a std::unique_ptr<int>
:
#include <memory>
int main() {
std::unique_ptr<int> Pointer {
std::make_unique<int>(42)
};
}
However, when using std::make_unique
, it’s somewhat common to use auto
type deduction:
#include <memory>
int main() {
auto Pointer{std::make_unique<int>(42)};
}
This is because the type of the underlying data (eg, int
) is included in the statement already. Additionally, make_unique
is so ubiquitous that C++ developers soon learn that it returns a unique_ptr
, so repeating this can add noise to our code.
Let’s see an example of unique pointers with a class. We’ll use this class throughout the rest of this lesson:
#include <iostream>
#include <memory>
class Character {
public:
std::string Name;
Character(std::string Name = "Frodo") :
Name { Name }
{
std::cout << "Creating " << Name << '\n';
}
~Character() {
std::cout << "Deleting " << Name << '\n';
}
};
int main() {
auto FrodoPointer{
std::make_unique<Character>("Frodo")
};
auto GandalfPointer{
std::make_unique<Character>("Gandalf")
};
}
This program outputs the following:
Creating Frodo
Creating Gandalf
Deleting Gandalf
Deleting Frodo
As with basic pointers, which are often referred to as "raw" pointers, we can dereference smart pointers and access the object they point to using the *
or ->
operators:
#include <iostream>
#include <memory>
class Character {/*...*/};
int main(){
auto FrodoPointer{
std::make_unique<Character>("Frodo")
};
std::cout << "Logging "
<< (*FrodoPointer).Name << "\n\n";
auto GandalfPointer{
std::make_unique<Character>("Gandalf")
};
std::cout << "Logging "
<< GandalfPointer->Name << "\n\n";
}
Creating Frodo
Logging Frodo
Creating Gandalf
Logging Gandalf
Deleting Gandalf
Deleting Frodo
Given the design intent of unique pointers, it doesn’t make sense to copy them directly. The std::unique_ptr
class protects against this by preventing its objects from being copied.
We will cover how to implement this capability for our own classes later, thereby controlling (or preventing) the process whereby objects of our custom types are copied.
We can see this in action by trying to copy a std::unique_ptr
:
#include <memory>
int main(){
auto Ptr1{std::make_unique<int>(42)};
auto Ptr2{Ptr1};
}
error: 'std::unique_ptr': attempting to reference a deleted function
When working with functions, passing by value is also a form of copying, so this will also be prevented with a similar error message:
#include <memory>
void SomeFunction(std::unique_ptr<int> Num) {
// ...
}
int main(){
auto Ptr{std::make_unique<int>(42)};
SomeFunction(Ptr);
}
error: 'std::unique_ptr': attempting to reference a deleted function
Of course, there are countless situations where we need to pass a pointer to a function. For those scenarios, smart pointers implement the get()
function, which returns the underlying raw pointer.
This allows other parts of our code to access our objects, without creating copies of our unique pointers:
#include <memory>
void SomeFunction(int* Num) {
// ...
}
int main() {
auto Ptr{std::make_unique<int>(42)};
SomeFunction(Ptr.get());
}
Within the memory ownership paradigm, we can imagine that any function that has a raw pointer to a resource simply has requesting access to that resource, but does not own it. The resource is owned by whoever has the unique_ptr
.
For scenarios where we want to transfer ownership of the resource, we can use std::move()
.
std::move()
Sometimes, we want to transfer ownership of a resource from one unique pointer to another.
This is where std::move()
comes in handy. It's a function available in the <utility>
header that allows us to transfer ownership of a unique pointer.
This mechanism allows us to transfer ownership of resources between different parts of our program, ensuring that at any given time, there's only one owner of the resource.
#include <memory>
#include <utility>
#include <iostream>
void TakeOwnership(std::unique_ptr<int> Num) {
std::cout
<< "TakeOwnership function now owns the pointer.\n";
std::cout << "Value: " << *Num << '\n';
}
int main() {
auto Number{std::make_unique<int>(42)};
std::cout << "main function owns the pointer.\n";
TakeOwnership(std::move(Number));
// Number is now in a "moved-from" state
if (Number == nullptr) {
std::cout << "Number no longer owns any object.\n";
}
}
main function owns the pointer.
TakeOwnership function now owns the pointer.
Value: 42
Number no longer owns any object.
In this example:
unique_ptr
called Number
in the main
function.std::move()
to transfer ownership of the integer to the TakeOwnership
function.Number
in main
no longer owns any object (it becomes a null pointer).It's important to note that after using std::move()
, the original pointer (Number
in this case) is left in a valid but unspecified state. It's safe to reassign it or let it go out of scope, but you shouldn't try to use the object it previously owned.
#include <memory>
#include <utility>
#include <iostream>
void TakeOwnership(std::unique_ptr<int> Num) {}
int main() {
auto Number{std::make_unique<int>(42)};
TakeOwnership(std::move(Number));
std::cout << *Number;
}
Most compilers can detect this, and will generate a warning:
Warning: Use of a moved from object: 'Number'
const
Unique PointersSimilar to raw pointers, there are two ways we can use const
with std::unique_ptr
. This creates four possible combinations:
Most of the examples we’ve seen in this lesson have been like this. Neither the pointer nor the object it points to are const
, so we can modify either:
#include <memory>
int main() {
auto Pointer{std::make_unique<int>(42)};
// Modify the underlying object
(*Pointer)++;
// Modify the pointer
Pointer.reset();
}
Here, we cannot modify the pointer, but we can modify the object it points to:
#include <memory>
int main() {
const auto Pointer{
std::make_unique<int>(42)};
// Modify the underlying object
(*Pointer)++;
// Error - can't modify the pointer
Pointer.reset();
}
Here, we can modify the pointer, but not the object it points to:
#include <memory>
int main() {
auto Pointer{
std::make_unique<const int>(42)};
// Error - can't modify the underlying object
(*Pointer)++;
// Modify the pointer
Pointer.reset();
}
Finally, both the pointer and the underlying object can be const
, preventing us from modifying either:
#include <memory>
int main() {
const auto Pointer{
std::make_unique<const int>(42)};
// Error - can't modify the underlying object
(*Pointer)++;
// Error - can't modify the pointer
Pointer.reset();
}
In this lesson, we delved into the concept of unique pointers, a crucial component of modern memory management. Let's briefly recap the key points:
std::make_unique
: We now know how to create unique pointers using std::make_unique
,get()
. We also covered how to transfer ownership of resources using std::move()
.Learn how to manage dynamic memory using unique pointers and the concept of memory ownership
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way