the flattening
not to be confused with the Minecraft technical update I'm moving about the pages to be have a more flat, less nested structure. I feel like that'll improve URLs a lot, making them more readable than ever. over the years I've learned that flatter is better, and that tagging is generally a much more effective way of organising things. this doesn't get rid of categories entirely, as I think having the category makes the URL much a bit more readable in the end. my current vision does include a concept of "major categories" either way.
This commit is contained in:
parent
20e29e3b2c
commit
39a6155bdc
33 changed files with 63 additions and 120 deletions
415
content/programming/cxx/shared-unique-ptr-deleter.tree
Normal file
415
content/programming/cxx/shared-unique-ptr-deleter.tree
Normal file
|
@ -0,0 +1,415 @@
|
|||
%% title = "freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`"
|
||||
|
||||
% id = "01J0VN48B2E9WZ4QW0X69N2KB8"
|
||||
- say you need to interface with a C library such as SDL2 in your C++ code
|
||||
|
||||
% id = "01J0VN48B2Z5BFFEZCEYG63662"
|
||||
- obviously the simplest way would be to just use the C library.
|
||||
|
||||
```cpp
|
||||
int main(void)
|
||||
{
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
|
||||
SDL_Window* window = SDL_CreateWindow(
|
||||
"Hello, world!",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
800, 600,
|
||||
0
|
||||
);
|
||||
|
||||
bool running = true;
|
||||
while (running) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_DestroyWindow(window);
|
||||
}
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2S4DSTR8DAP70DMH7"
|
||||
- this approach has the nice advantage of being really simple, but it doesn't work well if you build your codebase on RAII.
|
||||
|
||||
% id = "01J0VN48B2CT2DVHEB1HGK8KB7"
|
||||
- and as much as I disagree with using it _everywhere_ and injecting object-oriented design into everything, RAII is actually really useful for OS resources such as an `SDL_Window*`.
|
||||
|
||||
% id = "01J0VN48B2SX6GX0B3AKDVHGFX"
|
||||
- to make use of RAII you might be tempted to wrap your `SDL_Window*` in a class with a destructor…
|
||||
|
||||
```cpp
|
||||
struct window
|
||||
{
|
||||
SDL_Window* raw = nullptr;
|
||||
|
||||
window(const char* title, int x, int y, int w, int h, int flags)
|
||||
: raw(SDL_CreateWindow(title, x, y, w, h, flags))
|
||||
{}
|
||||
|
||||
~window()
|
||||
{
|
||||
if (raw != nullptr) {
|
||||
SDL_DestroyWindow(raw);
|
||||
raw = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2T6TQXD89EAGVNZ00"
|
||||
+ but remember the rule of three - if you declare a destructor, you pretty much always also want to declare a copy constructor, and a copy assignment operator
|
||||
|
||||
% id = "01J0VN48B23W9AQ4KDKS6KW530"
|
||||
- the rule of three says that
|
||||
|
||||
> If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.
|
||||
|
||||
from [cppreference.com](https://en.cppreference.com/w/cpp/language/rule_of_three#Rule_of_three), retrieved 2024-06-20 21:13 UTC+2
|
||||
|
||||
% id = "01J0VN48B209GB3N077D0TMV1K"
|
||||
- imagine a situation where you have a class managing a raw pointer like our `window`.
|
||||
|
||||
% id = "01J0VN48B296B7841YR2406YVJ"
|
||||
- what will happen with an explicit destructor, but a default copy constructor and copy assignment operator, is that upon copying an instance of the object, the new object will receive the same pointer as the original -
|
||||
and _its_ destructor will run to delete the pointer, _in addition to_ the destructor that will run to delete our original object - causing a double free!
|
||||
|
||||
% id = "01J0VN48B2E56P1B1TE903383P"
|
||||
- therefore we need a copy constructor to create a new allocation that will be freed by the second destructor.
|
||||
|
||||
% id = "01J0VN48B2E1X2G415P1TNG4CJ"
|
||||
- copying windows doesn't really make sense, so we can delete the copy constructor and copy assignment operator…
|
||||
|
||||
```cpp
|
||||
struct window
|
||||
{
|
||||
// -- snip --
|
||||
|
||||
window(const window&) = delete;
|
||||
void operator=(const window&) = delete;
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2R5ZAZGHBJ9H7E8PX"
|
||||
- that alone is cool, but it would be nice if we could move a `window` to a different location in memory instead of having to keep it in place.
|
||||
|
||||
% id = "01J0VN48B2W0SND10H6CK1MGJD"
|
||||
- having a copy constructor inhibits the compiler from creating a default move constructor and move assignment operator.
|
||||
|
||||
% id = "01J0VN48B2AAD3SKFWNDMYV4FV"
|
||||
- so we'll also want an explicit move constructor and a move assignment operator:
|
||||
|
||||
```cpp
|
||||
struct window
|
||||
{
|
||||
// -- snip --
|
||||
|
||||
window(window&& other)
|
||||
{
|
||||
raw = other.raw;
|
||||
other.raw = nullptr;
|
||||
}
|
||||
|
||||
window& operator=(window&& other)
|
||||
{
|
||||
raw = other.raw;
|
||||
other.raw = nullptr;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2B2VJNGC13ZGA2BAT"
|
||||
+ this fulfills the rule of five, which says that if you follow the rule of three and would like the object to be movable, you will want a move constructor and move assignment operator.
|
||||
|
||||
% id = "01J0VN48B2TMH1Z81YMPBGD1TA"
|
||||
- > Because the presence of a user-defined (or `= default` or `= delete` declared) destructor, copy-constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions: […]
|
||||
|
||||
from [cppreference.com](https://en.cppreference.com/w/cpp/language/rule_of_three#Rule_of_five), retrieved 2024-06-20 21:13 UTC+2
|
||||
|
||||
% id = "01J0VN48B2TFMXQRPPKJXEEX2E"
|
||||
- with all of this combined, our final `window` class looks like this:
|
||||
|
||||
```cpp
|
||||
struct window
|
||||
{
|
||||
SDL_Window* raw = nullptr;
|
||||
|
||||
window(const char* title, int x, int y, int w, int h, int flags)
|
||||
: raw(SDL_CreateWindow(title, x, y, w, h, flags))
|
||||
{}
|
||||
|
||||
~window()
|
||||
{
|
||||
if (raw != nullptr) {
|
||||
SDL_DestroyWindow(raw);
|
||||
raw = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
window(const window&) = delete;
|
||||
void operator=(const window&) = delete;
|
||||
|
||||
window(window&& other)
|
||||
{
|
||||
raw = other.raw;
|
||||
other.raw = nullptr;
|
||||
}
|
||||
|
||||
window& operator=(window&& other)
|
||||
{
|
||||
raw = other.raw;
|
||||
other.raw = nullptr;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2TJY2A9JEMR13QZGJ"
|
||||
- and with this class, our simple _Hello, world!_ program becomes this:
|
||||
|
||||
```cpp
|
||||
int main(void)
|
||||
{
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
|
||||
window window{
|
||||
"Hello, world!",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
800, 600,
|
||||
0,
|
||||
};
|
||||
|
||||
bool running = true;
|
||||
while (running) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
% id = "01J0VN48B23BDDNSDMJJR7CYWZ"
|
||||
- quite a bit of boilerplate just to call save a single line of code, isn't it?
|
||||
|
||||
% id = "01J0VN48B2Q2R1JP59FQBBCMBQ"
|
||||
+ we blew up our single line into 32. good job, young C++ programmer!
|
||||
|
||||
% id = "01J0VN48B232NV2XEVQ84SEVP9"
|
||||
- opinion time: you might be tempted to say that having this class makes it easy to provide functions that will query information about the window.
|
||||
|
||||
% id = "01J0VN48B2JTJF40ZTKZJS4VWC"
|
||||
- my argument is that in most cases you shouldn't create such functions, because the ones from SDL2 already exist.
|
||||
|
||||
% id = "01J0VN48B2WZ9PAX0W5W2ZMPPN"
|
||||
- albeit I'll admit that writing
|
||||
|
||||
```cpp
|
||||
int width;
|
||||
SDL_GetWindowSize(&window, &width, nullptr);
|
||||
```
|
||||
|
||||
just to obtain the window width does _not_ spark joy.
|
||||
|
||||
% id = "01J0VN48B2DCN9PPHHC818NPMD"
|
||||
- on the other hand it being this verbose does suggest that _maybe_ it's a little expensive to call, so there's that.
|
||||
|
||||
maybe save it somewhere and reuse it during a frame.
|
||||
I dunno, I'm not your dad to be telling you what to do.
|
||||
|
||||
neither have I read the SDL2 source code to know how expensive this function is, but the principle of least surprise tells me it should always return the _current_ window size, so I assume it always asks the OS.
|
||||
|
||||
% id = "01J0VN48B2HKNVRSS0DR67NCRF"
|
||||
- but the fine folks designing the C++ standard library have already thought of this use case.
|
||||
this is what _smart pointers_ are for after all - our good friends `std::shared_ptr` and `std::unique_ptr`, which `delete` things for us when they go out of scope, automatically!
|
||||
|
||||
% id = "01J0VN48B25A4W8MSMVNN6SZXF"
|
||||
- let's start with `std::shared_ptr` because it's a bit simpler.
|
||||
|
||||
% id = "01J0VN48B2AKGFBCA25TZXYNNZ"
|
||||
- `std::shared_ptr` is a simple form of _garbage collection_ - it will free its associated allocation once there are no more referencers to it.
|
||||
|
||||
% id = "01J0VN48B2WHH9KFASATVZ44FW"
|
||||
- naturally it has to know _how_ to perform the freeing.
|
||||
the standard library designers could have just assumed that all allocations are created with `new` and deleted with `delete`, but unfortunately the real world is not so simple.
|
||||
we have C libraries to interface with after all, and there destruction is accomplished simply by calling functions!
|
||||
|
||||
% id = "01J0VN48B2FE3TJ3QF6MZA4YYN"
|
||||
+ not to mention polymorphism - `delete` does not have any metadata about the underlying type. it calls the destructor of the _static_ type, which wouldn't work very well if the actual type was something else.
|
||||
|
||||
% id = "01J0VN48B266DE4H23789JHJYP"
|
||||
- (this is why having a `virtual` method in your polymorphic class requires your destructor to become `virtual`, too.)
|
||||
|
||||
% id = "01J0VN48B25AQ84D6Y682DRRQ0"
|
||||
- because of this, `std::shared_ptr` actually stores a _deleter_ object, whose sole task is to destroy the shared pointer's contents once there are no more references to it.
|
||||
|
||||
% id = "01J0VN48B2NBTZ62YDNKMDN1CC"
|
||||
- to set a custom deleter for an `std::shared_ptr`, we provide it as the 2nd argument of the constructor.
|
||||
so to automatically free our `SDL_Window` pointer, we would do this:
|
||||
|
||||
```cpp
|
||||
int main(void)
|
||||
{
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
|
||||
std::shared_ptr<SDL_Window> window{
|
||||
SDL_CreateWindow(
|
||||
"Hello, world!",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
800, 600,
|
||||
0
|
||||
),
|
||||
SDL_DestroyWindow,
|
||||
};
|
||||
|
||||
bool running = true;
|
||||
while (running) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and that's all there is to it!
|
||||
|
||||
% id = "01J0VN48B2WHNDKVBSDJRTYG4T"
|
||||
+ this is pretty much the simplest solution to our problem - it does not require declaring any additional types or anything of that sort.
|
||||
this is the solution I would go with in a production codebase.
|
||||
|
||||
% id = "01J0VN48B2KMWYV9C9GKJE22FK"
|
||||
- this is despite `std::shared_ptr`'s extra reference counting semantics -
|
||||
having formed somem Good Memory Management habits in Rust, I tend to shape my memory layout into a _tree_ rather than a _graph_, so to pass the window to the rest of the program I would pass an `SDL_Window&` down in function arguments.
|
||||
then only `main` has to concern itself with how the `SDL_Window`'s memory is managed.
|
||||
|
||||
% id = "01J0VN48B2E36EQ0HCBNR4HJ49"
|
||||
- using `std::shared_ptr` does have a downside though, and it's that there is some extra overhead associated with handling the shared pointer's _control block_.
|
||||
|
||||
% id = "01J0VN48B27DE35N204ZD1QJ8G"
|
||||
+ the control block is an additional area in memory that stores metadata about the shared pointer -
|
||||
the strong reference count, the [weak](https://en.cppreference.com/w/cpp/memory/weak_ptr) reference count, as well as our deleter.
|
||||
|
||||
% id = "01J0VN48B2HR38E85V5P1B81RH"
|
||||
- an additional thing to note is that when you're constructing an `std::shared_ptr` from an existing raw pointer, C++ cannot allocate the control block together with the original allocation.
|
||||
this can reduce cache locality if the allocator happens to place the control block very far from the allocation we want to manage through the shared pointer.
|
||||
|
||||
% id = "01J0VN48B2MR87BNJJYNAB7RBD"
|
||||
- we can avoid all of this overhead by using a `std::unique_ptr`, albeit not without some boilerplate.
|
||||
(spoiler: it's still way better than our original example though!)
|
||||
|
||||
% id = "01J0VN48B28X6H3KT9TAS4YEYE"
|
||||
- an `std::unique_ptr` stores which deleter to use as part of its template arguments - you may have never noticed, but `std::unique_ptr` is defined with an additional `Deleter` argument in its signature:
|
||||
|
||||
```cpp
|
||||
template <typename T, typename Deleter = std::default_delete<T>>
|
||||
class unique_ptr
|
||||
{
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2XN370CB5ZHR56VAM"
|
||||
- unfortunately for us, adding a deleter to an `std::unique_ptr` is not as simple as adding one to an `std::shared_ptr`, because it involves creating an additional type -
|
||||
we cannot just pass `SDL_DestroyWindow` into that argument, because that's a _function_, not a _type_.
|
||||
|
||||
% id = "01J0VN48B2XGJHN29N0D3BBVYV"
|
||||
- writing a little wrapper that will call `SDL_DestroyWindow` (or really any static function) for us is a pretty trivial task though:
|
||||
|
||||
```cpp
|
||||
template <typename T, void (*Deleter)(T*)>
|
||||
class function_delete
|
||||
{
|
||||
void operator()(void* allocation) const
|
||||
{
|
||||
Deleter(static_cast<T*>(allocation));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B24XPKQR7D0MZ1G005"
|
||||
- now we can delete an `SDL_Window` using our custom deleter like so:
|
||||
|
||||
```cpp
|
||||
std::unique_ptr<SDL_Window, function_delete<SDL_Window, SDL_DestroyWindow>> window{
|
||||
SDL_CreateWindow(
|
||||
"Hello, world!",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
800, 600,
|
||||
0
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2F1J2KW68RCGWS2S8"
|
||||
- having to type this whole type out every single time we want to refer to an owned `SDL_Window` is a bit of a pain though, so we can create a type alias:
|
||||
|
||||
```cpp
|
||||
namespace sdl
|
||||
{
|
||||
using window = std::unique_ptr<SDL_Window, function_delete<SDL_Window, SDL_DestroyWindow>>;
|
||||
}
|
||||
|
||||
sdl::window window{
|
||||
SDL_CreateWindow(
|
||||
"Hello, world!",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
800, 600,
|
||||
0
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2MZCBPQM24HR72V3B"
|
||||
- and having to repeat `SDL_Window` twice in the type alias is no fun, so we can create a type alias for `std::unique_ptr<T, function_delete<T, Deleter>>` too:
|
||||
|
||||
```cpp
|
||||
template <typename T, void (*Deleter)(T*)>
|
||||
using c_unique_ptr = std::unique_ptr<T, function_delete<T, Deleter>>;
|
||||
|
||||
namespace sdl
|
||||
{
|
||||
using window = c_unique_ptr<SDL_Window, SDL_DestroyWindow>;
|
||||
}
|
||||
```
|
||||
|
||||
…you get the idea.
|
||||
|
||||
% id = "01J0VN48B26EAF7D447F0RX8HB"
|
||||
- I'm calling it `c_unique_ptr` by the way because it's a _unique pointer to a C resource_.
|
||||
|
||||
% id = "01J0VN48B2MYPG61F125HV9N9T"
|
||||
- the unfortunate downside to this approach is that you can get pretty abysmal template error messages upon type mismatch:
|
||||
|
||||
```cpp
|
||||
void example(const sdl::window& w);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
example(1);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```diagnostics-clang
|
||||
sdl2.cpp:36:5: error: no matching function for call to 'example'
|
||||
36 | example(1);
|
||||
| ^~~~~~~
|
||||
sdl2.cpp:21:6: note: candidate function not viable: no known conversion from 'int' to 'const sdl::window' (aka 'const unique_ptr<SDL_Window, free_fn<SDL_Window, &SDL_DestroyWindow>>') for 1st argument
|
||||
21 | void example(const sdl::window& w);
|
||||
| ^ ~~~~~~~~~~~~~~~~~~~~
|
||||
1 error generated.
|
||||
```
|
||||
|
||||
% id = "01J0VN48B2CAAZFVHNRX34JRPB"
|
||||
- but hey, at least you avoid the overhead of reference counting - by making it completely unnecessary!
|
||||
move semantics ftw!
|
Loading…
Add table
Add a link
Reference in a new issue