fixity fix
This commit is contained in:
parent
eb6f2abdd5
commit
a5773b47e4
|
@ -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`
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue