fixity fix

This commit is contained in:
liquidex 2024-06-20 22:24:12 +02:00
parent eb6f2abdd5
commit a5773b47e4
3 changed files with 54 additions and 0 deletions

View file

@ -5,7 +5,9 @@
% content.link = "programming/languages/cxx/access-modifiers-as-labels" % content.link = "programming/languages/cxx/access-modifiers-as-labels"
redirect_from = ["programming/cxx/access-modifiers-as-labels"] redirect_from = ["programming/cxx/access-modifiers-as-labels"]
id = "01J0VN48AZGGM35KT8ANEA2B9Q"
+ :page: access modifiers as labels (`private:`, `protected:`, and `public:`) + :page: access modifiers as labels (`private:`, `protected:`, and `public:`)
% content.link = "programming/languages/cxx/shared-unique-ptr-deleter" % content.link = "programming/languages/cxx/shared-unique-ptr-deleter"
id = "01J0VN48AZYH6KJGK7PSKN0PCA"
+ :page: freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr` + :page: freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`

View file

@ -1,7 +1,9 @@
%% title = "freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`" %% 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 - 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. - obviously the simplest way would be to just use the C library.
```cpp ```cpp
int main(void) int main(void)
@ -29,10 +31,13 @@
} }
``` ```
% 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. - 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*`. - 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… - to make use of RAII you might be tempted to wrap your `SDL_Window*` in a class with a destructor…
```cpp ```cpp
@ -54,21 +59,27 @@ struct window
}; };
``` ```
% 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 + 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 - 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. > 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 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`. - 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 - - 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! 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. - 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… - copying windows doesn't really make sense, so we can delete the copy constructor and copy assignment operator…
```cpp ```cpp
struct window struct window
@ -80,10 +91,13 @@ struct window
}; };
``` ```
% 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. - 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. - 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: - so we'll also want an explicit move constructor and a move assignment operator:
```cpp ```cpp
struct window struct window
@ -105,12 +119,15 @@ struct window
}; };
``` ```
% 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. + 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: […] - > 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 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: - with all of this combined, our final `window` class looks like this:
```cpp ```cpp
struct window struct window
@ -147,6 +164,7 @@ struct window
}; };
``` ```
% id = "01J0VN48B2TJY2A9JEMR13QZGJ"
- and with this class, our simple _Hello, world!_ program becomes this: - and with this class, our simple _Hello, world!_ program becomes this:
```cpp ```cpp
@ -173,14 +191,19 @@ struct window
} }
``` ```
% id = "01J0VN48B23BDDNSDMJJR7CYWZ"
- quite a bit of boilerplate just to call save a single line of code, isn't it? - 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! + 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. - 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. - 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 - albeit I'll admit that writing
```cpp ```cpp
int width; int width;
@ -188,6 +211,7 @@ struct window
``` ```
just to obtain the window width does _not_ spark joy. 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. - 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. maybe save it somewhere and reuse it during a frame.
@ -195,23 +219,31 @@ struct window
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. 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. - 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! 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. - 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. - `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. - 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. 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! 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. + 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.) - (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. - 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. - 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: so to automatically free our `SDL_Window` pointer, we would do this:
```cpp ```cpp
@ -242,24 +274,31 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
``` ```
and that's all there is to it! 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 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. 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 - - 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. 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. 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_. - 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 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. 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. - 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. 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. - 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!) (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: - 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 ```cpp
@ -270,9 +309,11 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
}; };
``` ```
% 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 - - 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_. 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: - writing a little wrapper that will call `SDL_DestroyWindow` (or really any static function) for us is a pretty trivial task though:
```cpp ```cpp
@ -286,6 +327,7 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
}; };
``` ```
% id = "01J0VN48B24XPKQR7D0MZ1G005"
- now we can delete an `SDL_Window` using our custom deleter like so: - now we can delete an `SDL_Window` using our custom deleter like so:
```cpp ```cpp
@ -299,6 +341,7 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
}; };
``` ```
% 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: - 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 ```cpp
@ -317,6 +360,7 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
}; };
``` ```
% 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: - 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 ```cpp
@ -331,8 +375,10 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
…you get the idea. …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_. - 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: - the unfortunate downside to this approach is that you can get pretty abysmal template error messages upon type mismatch:
```cpp ```cpp
@ -356,5 +402,6 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_
1 error generated. 1 error generated.
``` ```
% id = "01J0VN48B2CAAZFVHNRX34JRPB"
- but hey, at least you avoid the overhead of reference counting - by making it completely unnecessary! - but hey, at least you avoid the overhead of reference counting - by making it completely unnecessary!
move semantics ftw! move semantics ftw!

View file

@ -13,12 +13,16 @@
% id = "issue-list" % id = "issue-list"
- ## issue list - ## issue list
% id = "01J0VN48BRABQ11Z1CE8EDQXXS"
+ :TODO: :l_feat: add page backreferences + :TODO: :l_feat: add page backreferences
% id = "01J0VN48BRGF0YD16Q7XWE5BPS"
- sometimes it's useful to see which pages link to a specific page - sometimes it's useful to see which pages link to a specific page
% id = "01J0VN48BRFM9DDP9KGZF4RGAR"
+ :TODO: :l_dev: replace Handlebars with something simpler and smaller + :TODO: :l_dev: replace Handlebars with something simpler and smaller
% id = "01J0VN48BR9299AB13A8FR2SF4"
- I don't need this many dependencies with this little customizability thank you - I don't need this many dependencies with this little customizability thank you
% id = "01J095FBXRC760YT7PZWWXQCMT" % id = "01J095FBXRC760YT7PZWWXQCMT"
@ -53,6 +57,7 @@
% id = "01J093FGZFGBDJ5QPHSZW9NVB5" % id = "01J093FGZFGBDJ5QPHSZW9NVB5"
+ :TODO: :l_content: [page:programming/projects] needs the rest of my projects + :TODO: :l_content: [page:programming/projects] needs the rest of my projects
% id = "01J0VN48BR5XZSHC0D6Q3DRG8P"
- I haven't had the motivation (or a reason) to talk about my projects there yet so yeah. - I haven't had the motivation (or a reason) to talk about my projects there yet so yeah.
% id = "01J093FGZF0D919Q1CS67SR4S2" % id = "01J093FGZF0D919Q1CS67SR4S2"