Manage episode 215295098 series 125918
Some people say that raw pointers are evil and should be avoided. Raw pointers are useful when used properly. This episode explains how to use raw pointers along with smart pointers and is taken from a recent game development session.
I have a text-based role-playing adventure game that I’m working on with a main hero in the game. I want the hero to be able to move around. It wouldn’t be very adventurous if the hero had to stay in the same spot. This means I need to keep track of the hero’s location. Because this is a simple game, the location just needs an x and y coordinate. I decided to add a class that would manage multiple groups of properties. Grouping them together seemed like a good idea so it’ll be easier to understand an x property and a y property if they appear in a group called location.
Listen to the full episode to understand where and why I chose to use unique_ptr to manage ownership of the groups and property values as well as how I used raw pointers when retrieving groups and property values. Or you can read the full transcript below.
Raw pointers are useful when used properly. Avoiding them entirely would be like a home improvement store deciding to stop selling hammers because too many people were hitting their thumbs.
If you use raw pointers the wrong way, then you will get hurt. It takes a little bit extra thought and knowledge that I’ll explain here in order to use them safely.
Some languages have decided to remove pointers and I always felt that programmers were the ones to suffer. Specifically, it makes certain designs more clumsy when you try to avoid pointers or when you have no pointers available at all in your language.
I’ll also be using examples in this episode from a recent game development session. These are the kinds of real-world applications that I explore in the game development sessions. You can register to attend an upcoming session by going to takeupcode.com and clicking on the Classes link at the top of the page. Each session is reasonably priced and you’ll find them available almost every Saturday.
Here’s the scenario that led me to describe how to use raw pointers. Software changes over time and I’ve been known to completely change designs in just a few hours if needed. So the design that I’m about to describe will almost certainly change.
I have a text-based role-playing adventure game that I’m working on with a main hero in the game. I want the hero to be able to move around. It wouldn’t be very adventurous if the hero had to stay in the same spot. This means I need to keep track of the hero’s location. Because this is a simple game, the location just needs an x and y coordinate.
I have a Character class to represent the hero. Now I could just put a couple integer properties in the class for the x and y coordinates. But I know that I want the game to be extensible. So I’ll eventually need a flexible way to add properties that I don’t know about yet.
One good way to do this is to start now. In other words, don’t add fixed properties now and then later try to add dynamic properties later that can change. Anytime you find yourself with special cases where you treat one thing differently than another, you’re just adding extra complexity to your design.
In other words, even a simple solution paired up with a more complex solution will overall be more complicated than if everything just used the more elaborate solution. Consistency goes a long way to making something easy to understand and use.
So I decided to add a class that would manage multiple groups of properties. Grouping them together seemed like a good idea so it’ll be easier to understand an x property and a y property if they appear in a group called location.
Maybe there can be another group called health that will be used when I add the ability to fight monsters in the game.
You should also notice that the design seems to make sense. It’s easy to relate to. I haven’t mentioned pointers once yet in the design. So far, the only technical term has been integer. I want the location property values to be whole numbers, or integers.
The design consists of a PropertyContainer class that holds groups. Each group is represented by a PropertyGroup class and has a name such as location. And finally, the actual property values are represented by a PropertyValue class and also have a name such as x or y.
Alright, because we don’t know ahead of time what the group names will be or how many groups. And, by the way, the same thing applies to the named properties. We’ll need a dynamic data structure to hold them. Something that can grow as needed to hold the contents. We’re also going to need to be able to quickly find the group or property given it’s name.
A game is even more time sensitive than a regular application. So like I’ve said before, if you can program a game, then you can program almost anything. You need to become familiar with your options and be able to pick the right solution given the expected needs and requirements.
Looking up something quickly given a name is perfect for a dictionary or a map. The name acts as a string-based key. I’m using C++ for the game development, so I chose an unordered_map to hold the groups and properties. I’ll just refer to this as a map from now on.
The PropertyContainer class will have a map of strings to PropertyGroups. But here’s a question. Should the map contain actual PropertyGroup class instances? I don’t like to do this because it’s harder to share those instances with other code. If the map ever needs to grow, then it’s going to move things around. And what if I eventually want to mix PropertyGroups with other derived classes? The solution is to use pointers.
Using a pointer allows the map to contain a pointer to the actual PropertyGroup. Now, if the map needs to grow, it shifts around its pointers but the actual PropertyGroups stay put in memory. And because we’re using pointers, then the PropertyGroup being pointed to could actually be a derived class instance and it’ll continue to work as expected.
The same thing applies to how the PropertyGroups contain PropertyValues. They use their own map of property value string names to map to pointers of PropertyValue instances.
Now, what kind of pointers should we use for the maps? Using raw pointers here would be a bad idea. We’d have to write a lot of extra code to make sure the pointers got deleted when the group or value was no longer needed. And we still wouldn’t be able to guarantee it would work in all cases without leaking anything.
I could define the maps to hold shared pointers to the groups and values. But I feel this sets up the wrong expectation. A PropertyContainer should be the sole owner of all the PropertyGroups it contains. It should be fully responsible for them. And the same thing applies to the groups. Each PropertyGroup instance should be fully responsible for each PropertyValue instance.
So, I chose to have the maps contain unique pointers. So far, we’ve stayed well away from raw pointers. That’s normally a good thing. The title of this episode promised to show you how to use raw pointers properly. Not how to use raw pointers everywhere. In order to use raw pointers properly, it usually means very small roles under controlled conditions. They should be the exception to the rule.
What problems do raw pointers cause that they should be used rarely?
The first thing relates to ownership. What code should delete the pointer when it’s no longer needed? And how does it even know when it’s not needed anymore? A raw pointer doesn’t have anything to help answer these questions. So when you use raw pointers and start passing them around inside your code, you’ll often find bugs where some code deletes the pointer thinking it’s not needed anymore while another section still tries to use it. Or you get multiple deletes. Or the pointer never gets deleted. Ownership is a real problem because all a pointer really does is point to another memory location.
And what will you find at the memory location pointed to by a pointer. A raw pointer doesn’t tell you if that memory is still valid or not. It’s just an address. This is the next problem. Sure, you might have an idea of what type to expect. The compiler will help you here by keeping track of pointer types. But you don’t know if the object has been deleted or moved somewhere else in memory.
With all these problems, why use pointers in the first place? Well, the smart pointer types shared pointer and unique pointer solve many of these problems. Anytime you need to create objects in memory dynamically where you don’t know ahead of time how many you’ll need, then you’re going to need to work with pointers. Some languages that get rid of pointers call these references. But C++ makes a distinction between a reference and a pointer. A C++ reference always has to refer to something else. You can’t use a reference to refer to the object you just requested from memory. Once you have a pointer to the newly created object, then you can start using references. But your primary interaction with the newly created object will be through a pointer. Immediately giving this pointer to one of the smart pointer types to manage is a really good idea.
Because references always have to refer to something, you can’t have a reference to nothing. This is a problem for the property groups and property values. Because what should the code do if a caller asks for a property group by name and there is no group with that name?
If I had the code return a reference to the group or value that was found, then the code would have trouble when nothing was found. The only way to handle this would be to throw an exception. That’s not a bad design. Especially, if you expect this to rarely happen. Exceptions should also be used for exceptional cases.
But in a more dynamic environment where properties might come and go as extensions are added and removed, I wanted a design that was easy to deal with missing groups and properties. This might be something I change later. But for now, I wanted to be able to return something that would let the caller know that the name of the group or value could not be found.
A pointer can refer to nothing. This is called a null pointer. It’s really just a pointer that points to memory address zero. If you ever try to access anything at memory address zero, then your program will likely crash. This is a bug. As long as your code is aware that a pointer can be null and checks for this condition before trying to use the pointer, then there’s no problem.
You have this problem with smart pointers too. Just be aware that when working with pointers, they can sometimes point to null.
Now, because the PropertyContainer class keeps track of its PropertyGroups with unique pointers, it can’t return another unique pointer to the group. That would go against what it means to be unique. So I chose to have it return a raw pointer.
It works like this. You ask the property container for a group and get back a pointer to that group. If you ask for a group with a name that the container knows about, then you get back a valid raw pointer to that group. And if you ask for a group using an unknown name, then you get back a null pointer. The same thing applies to asking a group for a particular value by name.
The code that asks for the group or the value knows that it’s getting a raw pointer and should not try to delete the group or name. This is just a convention. It could call delete and that would be a bug. But at least it’s clear that the raw pointer gives no ownership rights to the objects. The code should not try to hold onto this pointer for later use or start passing it around to other places.
It should ask for a group or a value, check the returned pointer to make sure it’s not null, and then make use of that pointer right away. Once done, it should forget about the pointer. As long as the code follows this convention, then it’s safe to use raw pointers in this design.