From a5773b47e4119c494274155fedf54b53f5d41527 Mon Sep 17 00:00:00 2001 From: lqdev Date: Thu, 20 Jun 2024 22:24:12 +0200 Subject: [PATCH] fixity fix --- content/programming/languages/cxx.tree | 2 + .../cxx/shared-unique-ptr-deleter.tree | 47 +++++++++++++++++++ content/treehouse/issues.tree | 5 ++ 3 files changed, 54 insertions(+) diff --git a/content/programming/languages/cxx.tree b/content/programming/languages/cxx.tree index b9aa476..3440122 100644 --- a/content/programming/languages/cxx.tree +++ b/content/programming/languages/cxx.tree @@ -5,7 +5,9 @@ % content.link = "programming/languages/cxx/access-modifiers-as-labels" redirect_from = ["programming/cxx/access-modifiers-as-labels"] + id = "01J0VN48AZGGM35KT8ANEA2B9Q" + :page: access modifiers as labels (`private:`, `protected:`, and `public:`) % content.link = "programming/languages/cxx/shared-unique-ptr-deleter" + id = "01J0VN48AZYH6KJGK7PSKN0PCA" + :page: freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr` diff --git a/content/programming/languages/cxx/shared-unique-ptr-deleter.tree b/content/programming/languages/cxx/shared-unique-ptr-deleter.tree index 3a569e7..bac6c65 100644 --- a/content/programming/languages/cxx/shared-unique-ptr-deleter.tree +++ b/content/programming/languages/cxx/shared-unique-ptr-deleter.tree @@ -1,7 +1,9 @@ %% 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) @@ -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. + % 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 @@ -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 + % 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 @@ -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. + % 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 @@ -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. + % 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 @@ -147,6 +164,7 @@ struct window }; ``` + % id = "01J0VN48B2TJY2A9JEMR13QZGJ" - and with this class, our simple _Hello, world!_ program becomes this: ```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? + % 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; @@ -188,6 +211,7 @@ struct window ``` 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. @@ -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. +% 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 @@ -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! + % 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 @@ -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 - 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 @@ -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: ```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: ```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>` too: ```cpp @@ -331,8 +375,10 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_ …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 @@ -356,5 +402,6 @@ this is what _smart pointers_ are for after all - our good friends `std::shared_ 1 error generated. ``` + % id = "01J0VN48B2CAAZFVHNRX34JRPB" - but hey, at least you avoid the overhead of reference counting - by making it completely unnecessary! move semantics ftw! diff --git a/content/treehouse/issues.tree b/content/treehouse/issues.tree index 7e07ca6..09b82ae 100644 --- a/content/treehouse/issues.tree +++ b/content/treehouse/issues.tree @@ -13,12 +13,16 @@ % id = "issue-list" - ## issue list + % id = "01J0VN48BRABQ11Z1CE8EDQXXS" + :TODO: :l_feat: add page backreferences + % id = "01J0VN48BRGF0YD16Q7XWE5BPS" - 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 + % id = "01J0VN48BR9299AB13A8FR2SF4" - I don't need this many dependencies with this little customizability thank you % id = "01J095FBXRC760YT7PZWWXQCMT" @@ -53,6 +57,7 @@ % id = "01J093FGZFGBDJ5QPHSZW9NVB5" + :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. % id = "01J093FGZF0D919Q1CS67SR4S2"