Since we're on the specific subject of unique_ptr and SDL, this is an opportunity for an excellent discussion...
We can see in the naive implementation above from adam2016 (where here 'naive' is not a comment about adam2016 ; it's a common work used by programmers to describe a very simple implementation of something that doesn't really take advantage of the language) that all the SDL resources get initialised with a specific function call (for example,
SDL_CreateRenderer ) and are thence accessible with raw pointers.
Many of these resources should be released not with a simple delete, but with a call to a specific SDL function which presumably does some tidying up (for example,
SDL_DestroyRenderer ).
We can see in the code above that there is a cleanUp() function doing this; it calls the right deinitialisation function on the right pointers in the right order. If this code becomes more complicated, this will get more complicated. What if sometimes, some resources aren't always initialised? What if we make a mistake here where there are lots of resources and forget some, or do one twice, or get them in the wrong order? What if the order isn't fixed? What if the code throws an exception, and we skip over calling the cleanup? What if we remember to call the cleanup, but then how do we then know which of these resources even got initialised? Welcome to wide word of C-style problems with manual cleanup!
This sounds awfully familiar, doesn't it? A function used to initialise an object, and another function to be called when its time to tidy away that object? Goodness me - we're talking about a
constructor and a
destructor !
Now, we don't need to talk about what he pitfalls of having to manually initialise and manually deinitialise objects are; it's basically the reason why constructors and destructors exist. RAII is a big advantage of C++.
So here's one way that C++ programmers have traditionally dealt with having this sort of C-style manual construction and destruction of something - just wrapping it inside a C++ class that will handle that for us.
1 2 3 4 5 6 7 8 9 10
|
class SDLRendererWrapped
{
public:
SDL_Renderer* pRenderer;
SDLRendererWrapped(SDL_Window* window, int index, Uint32 flags)
{
pRenderer = SDL_CreateRenderer (window, index, flags);
}
~SDLRendererWrapped() {SDL_DestroyRenderer(pRenderer};
}
|
This now allows us to create a SDLRendererWrapped object just like we would have created a SDL_Renderer, and in this simple implementation we can easily reach the actual pointer to the actual renderer that we want. We could also overload various operators to make it behave more like an exact drop-in replacement. We have achieved memory safety, and we are guaranteed that when this object goes out of scope the rendered will be properly tided up, and so on. All the problems of manual memory management and manual deinitialisation of objects have been dealt with.
However, in C++11, we got smart pointers. A unique_ptr will, when it goes out of scope, call delete on the pointer it holds for us. If all this renderer object required was a call to delete when it was time to destroy it, this would be simple and easy. However, this rendered object requires more than that. It requires a call to SDL_DestroyRenderer when it's being deleted.
C++11 smart pointers allow us to do this; they allow us to tell the unique_ptr that calling delete isn't what's required. That instead, it has to call this other function.
This is what mbozzi is doing in his code example above.
mbozzi has done a quick and simple example using some preprocessor magic. Here's a page discussing exactly this example with SDL, without using any preprocessor magic, and covering shared_ptr as well;
https://swarminglogic.com/jotting/2015_05_smartwrappers
I won't go into the details. The important thing is to understand, and to add to your "how to think and design in C++" knowledge is thus two-fold:
1) When objects require specific initialisation and destruction, such as specific function calls, they are just begging to be wrapped inside a C++ object so that we get the benefits of RAII (i.e. memory safety, correct order of destruction, simpler code, etc.)
2) If that init and destruction is simple enough (as it is in this SDL case), that C++ object can be a simple smart pointer that we inform of the function that needs to be called upon destruction.
We can always look up the syntax for how to do this; what we can't look up is this extra knowledge that this is possible and desirable. The knowledge that this is possible and a good thing needs to go inside the programmer's head now so that the programmer designs code to use it where appropriate. We can always look up the syntax when you need it; this knowledge that it's possible has to go inside heads so it will be used in the future.
Don't focus on the syntax here; focus on internalising what's possible, and you'll be able to look up the syntax later (and one day you'll realise you've memorised it).
When people say "think in C++" this is the kind of thing they mean.