Whenever we write a function that accepts arguments, we generally have some assumptions about those arguments. For example, if we have a function that takes a pointer, we might assume that the pointer is not a nullptr
.
These are sometimes called "preconditions" - things that our function assumes to be true before it starts performing its task.
throw
, try
and catch
throw
, try
, and catch
.In the previous lesson, we introduced the assert()
macro, which gives us a way to check for errors at run time. This is useful for simple scenarios but, for more complex software, we also need a more powerful option. Specifically, we’d like to be able to do things like:
To implement this capability, we have dedicated keywords within C++, and many similar languages: throw
, try
, and catch
.
Rather than throwing simple primitive objects like integers and strings, our exceptions get much more powerful when we throw types that are specifically designed to represent errors.
This gives us the ability to define variables and class functions on our error objects, giving our throw
blocks many more options to inspect and react to the exception.
When we’re dealing with dynamic memory and smart pointers, interesting questions and problems soon begin to arise.
Consider the following setup. We have Character
objects who carry Sword
objects. When a character is created, it creates a sword for itself, and it dutifully cleans up the sword when it gets destroyed:
struct Sword {};
class Character {
public:
Character() : Weapon{new Sword{}} {}
~Character() { delete Weapon; }
Sword* Weapon;
};
However, problems will soon arise once we begin using this class. We’ll introduce some of the problems, and how to solve them throughout this lesson.
std::move()
In our previous lesson, we saw how we could implement copy constructors and operators to implement copy semantics. Here, we’re going to implement move semantics.
However, it’s helpful to first understand why we need this feature at all, so let’s introduce some of the problems it’s designed to solve.
Often, the objects we create will need to store other objects within them. These sub-objects can often be containers such as std::vector
objects, which themselves may contain many thousands of their own sub-objects.
For example, if we’re working with databases, we might have objects that are storing thousands of other objects, representing entries in that database.
If we were making a video game, we might have an object representing an entire level, comprised of thousands of sub-objects of various types (enemies, environment art, audio, and so on)
In the previous lesson on move semantics, we introduced a new type of reference, which uses the &&
 syntax:
#include <iostream>
struct Resource {
// Copy Constructor
Resource(const Resource& Source) {}
// Move Constructor
Resource(Resource&& Source) {}
};
In this lesson, we’ll explore what this means in more detail, by introducing value categories.
In this lesson, we’ll get SDL installed, along with two extensions which we’ll need later in this chapter.
SDL
is a cross-platform library that allows us to create windows, access input devices, and create graphics.SDL_image
is an extension that allows us to work with images in all the common formats, such as jpeg and pngSDL_ttf
is an extension that we can use to render text at run-timeThis setup guide is designed for projects that use the CMake build system, where the SDL libraries will be stored in a subdirectory of our project.
In this lesson, we’ll get SDL installed, along with two extensions which we’ll need later in this chapter.
SDL
is a cross-platform library that allows us to create windows, access input devices, and create graphics.SDL_image
is an extension that allows us to work with images in all the common formats (jpeg, png, etc)SDL_ttf
is an extension that we can use to render text at run-timeThis setup guide downloads the source code for the libraries and compiles them on our local machine. We then demonstrate how to add these libraries to projects that use either Xcode or CMake
std::unique_ptr
std::unique_ptr
in C++In the world of long-running, complex software like online game servers, managing memory efficiently is critical.
Imagine handling hundreds of thousands of objects without a hitch, for weeks or even months. Here, even a tiny memory leak can snowball into a major issue.
That's where strategic memory management comes in. Instead of littering your code with haphazard delete
calls, we adopt a system of ownership. In this system, objects own other objects, creating a hierarchical structure of responsibility.
This approach not only cleans up our code but also ensures that when one object is deleted, all its dependents are automatically cleaned up too.
Let's dive into how we can simplify and strengthen our memory management with this smart system of ownership.
std::shared_ptr
std::shared_ptr
So far in this section, we’ve coverered how smart pointers are tools for managing dynamically allocated memory, ensuring that memory is automatically deallocated when it is no longer needed.
The C++ standard library provides a few variations of smart pointers. std::unique_ptr
and std::shared_ptr
are the most commonly used, but they serve different purposes:
Unique pointers, such as std::unique_ptr
enforces unique ownership of the memory resource it manages. It implies that only one unique pointer can point to a specific resource at any time.
When the std::unique_ptr
is destroyed or revokes its ownership through the std::move()
, release()
or reset()
methods, the resource it points to is automatically deallocated. This type of smart pointer is lightweight and efficient, making it an ideal choice for most single-owner scenarios.
We covered unique pointers in detail a dedicated lesson:
Unlike std::unique_ptr
, std::shared_ptr
allows multiple pointers to share ownership of a single resource. The resource is only deallocated when the last std::shared_ptr
pointing to it is destroyed or reset.
This shared ownership is managed through reference counting - an internal mechanism that keeps track of how many std::shared_ptr
instances are managing the same resource. This comes at a performance cost, so in general, std::unique_ptr
, should be our default choice of smart pointer.
However, there are scenarios where an object needs to be accessed and managed by multiple owners. In such cases, std::shared_ptr
becomes invaluable, and we’ll cover it in detail in this lesson.
Similar to the previous lesson, we’ll be using the following Character
class to demonstrate how shared pointers work.
It has a simple constructor and destructor that logs when objects are created and destroyed, so we can better understand when these steps happen: