C++ - Reddit
226 subscribers
48 photos
8 videos
1 file
25.5K links
Stay up-to-date with everything C++!
Content directly fetched from the subreddit just for you.

Join our group for discussions : @programminginc

Powered by : @r_channels
Download Telegram
miniaudio compile times

i found this library called miniaudio, however instead of having precompiled dll files, its just the header and then the c file that really only includes the header, the header file has all of the definitions and declarations of everything in the library, it makes the compile times take a lot of time, can i compile the header file to a dll the c file is literally just:

#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio/miniaudio.h"

and the header file is like 4 megabytes

https://redd.it/1trk8yn
@r_cpp
Exotic CRTP: Enforcing Strict Interfaces Without Friends Using C++23 Explicit Object Parameters

I’ve been experimenting with CRTP and ended up with a variation that enforces a strict interface/implementation boundary without friend declarations. The goal was to eliminate boilerplate I frequently encountered when trying to encapsulate derived class methods.

The key idea is using C++23 explicit object parameters this + a small access wrapper type so implementations can only be called through the interface layer.

That was about two and a half months ago. Since, I’ve taken the time to better understand it and write an article about it, which you can find below.
As explained there, I refer to this approach as Exotic CRTP.

---

### Example

// Reference example of the pattern
// See: https://medium.com/@felixolivierdumas/exotic-crtp-rethinking-static-polymorphism-with-c-23-89f9e75e8ffd

#include <iostream>
#include <type_traits>
#include <utility>

namespace exotic {

template<typename... From>
struct crtp_access : From... {};

template<typename T>
constexpr decltype(auto) as_crtp(T&& obj) noexcept {
using crtp_access_t = crtp_access<std::remove_cvref_t<T>>;
return static_cast<crtp_access_t&&>(obj);
}

}

struct Base {
void interface(this auto&& self) {
exotic::as_crtp(self).implementation();
}
};

struct Derived : Base {
void implementation(this exotic::crtp_access<Derived> self) {
std::cout << "Derived implementation" << std::endl;
}
};

int main() {
Derived d;

d.interface(); // perfectly works

// d.implementation(); -> doesn't work, Derived only allows .interface()
}


---

Not sure yet if this is actually useful in real conditions or just a different way of structuring CRTP, but it seems to be genuinely powerful.

Full write-up here: https://medium.com/@felixolivierdumas/exotic-crtp-rethinking-static-polymorphism-with-c-23-89f9e75e8ffd

Curious how this compares to traditional CRTP + friend patterns in real codebases :)

https://redd.it/1tuv2pn
@r_cpp
LLMs with C and C++ - switch from HFT to AI lab

I'm a senior C++ dev recently started working with a neolab (who works with anthropic). Thought I would write some of observations i made.

* I had experimented with LLMs for a while before making the switch. LLMs fail a lot on C and C++ due to harder nature and powerful nature of language.
* Talking purely from benchmarks, languages like python and JS (the vibecoder's first language) have very hard benchmarks - think fixing actual bug that touches 3 different modules from scratch with access to tools like grep, cat and python3 executer.
* Whereas, benchmarks for C and C++ are at basic QA style questions. I have added a task from benchmark, which fable 5 could not solve.
* LLMs do not have understanding of latest ISO standards - for some reasons it switches to C++17 again and again
* LLMs are trash at template metaprogramming. Try debugging CRTP type of errors.

Looking at the efforts and progress, I am still not sure if we will see LLMs writing MRs to linux kernel. The approach they used for vibecoding languages do not care about memory safety, thread safety and performance much. It would be interesting to see the space evolve.

PS: a example from benchmark

PS2: i'm not associated with benchmark. they say the code is taken from real github issues.

Observe the following faulty CPP code snippet and error type list. Your task is to select the error type of the code based on the error list provided.
You only need to answer error type. Do not write anything else in your response.
For example, if the code snippet is missing a semicolon, Your output should be 'missing_colons'.
faulty code:
```cpp
#include <bits/stdc++.h>

int countPermutations(int n, int k, int qq[])
{
const int N = 505, P = 998244353;
int *q = new int[n + 10];
int m, dp[N][N], jc[N], f[N], ans;
memset(q, 0, sizeof(int) * (n + 1));
memset(dp, 0, sizeof(dp));
memset(jc, 0, sizeof(jc));
memset(f, 0, sizeof(f));
ans = 0;

for (int i = 1; i <= n; i++)
q[i] = qq[i - 1];
dp[0][0] = f[0] = 1;
for (int i = jc[0] = 1; i <= n; i++)
jc[i] = 1LL * jc[i - 1] * i % P;
for (int i = 1; i <= n; i++)
{
f[i] = jc[i];
for (int j = 1; j < i; j++)
f[i] = (f[i] + P - 1LL * f[j] * jc[i - j] % P) % P;
}
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < i; j++)
for (int k = 1; k <= n; k++)
dp[i][k] = (dp[i][k] + dp[j][k - 1] * 1LL * f[i - j] % P) % P;
}
m = 0;
for (int i = 1; i <= n; i++)
if (q[i] > q[i + 1])
{
m = i;
break;
}
if (m == n)
{
for (int i = k; i <= n; i++)
ans = (ans + dp[n][i]) % P;
}
else
{
for (int i = m + 1; i <= n; i++)
{
if (i != m + 1 && (q[i - 1] > q[i] || q[i] < q[m]))
break;
int c = k + i - n - 1;
if (c >= 0)
ans = (ans + dp[m][c] * 1LL * jc[i - m - 1] % P) % P;
}
}
return ans;
}
```
error list:
['Delayed Execution', 'Improper HTML structure', 'Missing $', 'Missing mut', 'Misused := and =', 'Misused === and ==', 'Misused =>', 'Misused Macro Definition', 'Misused Spread Operator', 'Misused begin/end', 'Misused match', 'Misused var and val', 'Unused Variable', 'algorithm_error', 'condition_error', 'double_bug', 'faulty_indexing', 'function_error', 'html_unclosed_label', 'html_value_error', 'html_wrong_label', 'illegal_comment', 'illegal_indentation', 'illegal_keyword', 'illegal_separation', 'json_content_error', 'json_digital_leader_is_0', 'json_duplicate keys', 'json_struct_error', 'markdown_content_error', 'markdown_title_error', 'markdown_unclosed_error', 'missing_backtick', 'missing_colons', 'misused ==and=',
A tiny C++23 filesystem wrapper that returns std::expected instead of throwing

Hi everyone,

I’ve been working on a small C++23 header-only library called **expected_fs**:

https://github.com/MrZLeo/expected-fs

It’s a thin wrapper around `std::filesystem` where fallible operations return `std::expected<T, std::error_code>` instead of throwing `std::filesystem_error`.

The idea is not to replace `std::filesystem`, but to make filesystem code easier to compose in projects that prefer explicit error handling.

Example:

```cpp
#include <expected_fs/expected_fs.hpp>

#include <iostream>

int main() {
const auto size = expected_fs::file_size("data.txt");

if (!size) {
std::cerr << size.error().message() << '\n';
return 1;
}

std::cout << *size << '\n';
}
```

Most function names mirror `std::filesystem`, so usage should feel familiar:

```cpp
auto created = expected_fs::create_directories("out/cache");
auto copied = expected_fs::copy_file("input.txt", "out/input.txt");
auto removed = expected_fs::remove("out/input.txt");
```


It's simple and it just work. I’d love feedback on the API shape, naming, CMake packaging, and whether this feels useful to people who avoid exception-based filesystem handling.

Thanks for taking a look! :)

https://redd.it/1ubq20l
@r_cpp
Tired of mutex hell and spaghetti event handlers? Here's a single-threaded FSM methodology that keeps your async logic clean — forever. Just #include "uniflow.hpp".

We've all been there. A module that starts simple —
connect, send, wait for ack — and six months later looks like this:

bool connecting_, connected_, cmd_sent_, waiting_ack_, draining_, fault_;

void Update() {
if (estop_) {
connecting_ = false;
cmd_sent_ = false;
waiting_ack_ = false;
// forgot draining_ — ghost ack bug waiting to happen
...
}
if (!connected_) { ... }
else if (!cmd_sent_) { ... }
else if (waiting_ack_) { ... }
// where does fault_ get handled? what if estop_ and fault_ fire together?
}

Flags that must move in pairs. Resets that get forgotten.
E-stop logic that has to know about every stage.
Two developers write the same flow in completely different shapes.

**uniflow** keeps what works about the tick-based FSM — cooperative,
single-thread, no mutex — and fixes what doesn't.

Each stage becomes a named step function:

StepResult Step1_Connect() { device_.BeginConnect();
return Next(UF_FN(Step2_WaitConnected)); }

StepResult Step2_WaitConnected() { if (!device_.IsConnected()) return Stay();
return Next(UF_FN(Step3_WaitRequest)); }

StepResult Step3_WaitRequest() { if (!input_.HasRequest()) return Stay();
device_.Send(input_.Take());
return Next(UF_FN(Step4_WaitAck)); }

StepResult Step4_WaitAck() { if (device_.HasAck()) return Done();
return StayUntil(3000ms, UF_FN(Step5_Timeout)); }

One function = one state. Entry is explicit. Transitions are pinned in code.
No hidden jumps. No forgotten resets. Brace depth stays flat forever.

---

**How it works**

One `Runtime` owns one pump thread. Attach as many modules as you want.
The pump visits each module once per round — cooperative, round-robin.

uniflow::Runtime rt;
Flow_XAxis x_axis{rt};
Flow_YAxis y_axis{rt};
Flow_Conveyor conveyor{rt};

x_axis.ctx_home_.StartFlow(); // start homing X
y_axis.ctx_home_.StartFlow(); // start homing Y — simultaneously, no thread, no mutex

Modules on the same Runtime share the single-thread invariant.
No locks needed. Ever.

The pump adapts its sleep to the situation:
- Back-to-back transitions? No sleep, full speed.
- Polling with Stay()? 20ms yield. CPU near 0.
- All idle? 1ms — ready to wake immediately.

External event arrives? Call `rt.Wake()` from any thread. No waiting out a sleep cycle.

---

**Blocking work? No problem.**

Heavy I/O goes to the built-in thread pool via SubmitAsync.
The pump never blocks. Every other module keeps running.

StepResult Step1_Fetch() {
AsyncId job = SubmitAsync(UF_FN(DoFetch), 5000ms, url_);
return Next(UF_FN(Step2_Process), job);
}

StepResult Step2_Process(AsyncId job) {
auto r = AsyncResult<std::string>(job);
if (r.pending()) return StayUntil(5000ms, UF_FN(Step_Timeout));
if (!r.ok()) return Fail();
data_ = *r.return_value;
return Next(UF_FN(Step3_Save));
}

---

**Built-in tracing — zero instrumentation code**

Because every execution is "a step function was called once,"
one measurement point inside the pump sees everything:

[JobWorker] FLOW START
[JobWorker] Entry -> Step2_Validate elapsed=0.01ms
[JobWorker] ASYNC SUBMIT CallApi
[JobWorker] ASYNC DONE CallApi wait=124.38ms
[JobWorker] Step2_Validate -> Step3_WaitSave elapsed=124.42ms
[JobWorker] FLOW END DONE wall=143.21ms

Which step, how long, where it slowed down — visible without touching your logic code.

---

**What it's not**

- Not Boost.Asio — no new type system to learn, zero deps, doesn't absorb your objects
- Not C++20 coroutines — C++17, and style is
Rate my code!

#include <stdio.h> #include <string.h> #include <iostream> #include <filesystem> using namespace std; namespace fs = std::filesystem; using S = string; using H = const unsigned char; using F = void()(); using G = S()(); using D = void()(fs::path); using Q = void()(S); template<int N, int K> struct Z { S d(H h) { S r; int key = K; for(int i = 0; i < N; i++) { r += char(h[i] ^ key); key = (key 0x5F + 0x13) & 0xFF; } for(char &c : r) c = (c ^ 0x7F) - 3; return r; } }; S b(H h, int n) { S s; for(int i=0;i<n;i++) s+=char(hi); return s; } struct Obf { void o(S x) { cout << x; } S i() { S t; cin >> t; return t; } }; bool d(S p) { return fs::isdirectory(p); } void l(fs::path p) { for(auto& e : fs::directoryiterator(p)) cout << e.path().filename().string() << '\n'; } Obf ob; void e() { ob.o(b((H)"\x73\x6f\x72\x72\x79\x20\x77\x65\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x66\x69\x6e\x64\x20\x79\x6f\x75\x72\x20\x64\x69\x72\x65\x63\x74\x6f\x72\x79\x0a",38)); } void r(S p) { d(p) ? l(p) : e(); } S g() { return ob.i(); } F vf32; G gf8; D df8; Q qf8; void s1() { ob.o(Z<76, 0x37>().d((H)"\x4f\x5a\x58\x4e\x4b\x4e\x20\x60\x65\x5e\x52\x52\x20\x5d\x5e\x20\x5e\x5e\x63\x5e\x20\x57\x5d\x5e\x55\x5e\x5e\x5f\x5e\x20\x5e\x5f\x20\x5e\x5e\x5f\x20\x27\x2e\x2e\x27\x20\x5a\x5e\x5f\x20\x60\x5d\x5d\x5e\x27\x60\x20\x5f\x63\x5f\x5f\x5e\x5e\x5f\x20\x57\x5d\x5e\x55\x5e\x5e\x5f\x0a")); S x = gf0; x == "CD" ? vf9 : qf0; } void s2() { vf10; } void s3() { df0); } void s4(S p) { qf1; } void s5() { ob.o(b((H)"\x0a",1)); } int main() { vf0 = s1; vf9 = s3; vf10 = s2; df0 = l; qf0 = r; qf[1] = s4; gf[0] = g; vf0; s5(); return 0; }

https://redd.it/1ukn1v3
@r_cpp
Binding int from variant to reference in Foo.

Hello guys, my friend had a problem where he couldn't bind an int to int reference in Foo struct held in variant:

struct Foo {
int &abc;
};

std::variant<Foo, std::string> v;

// You can't!
v = Foo {
.abc = ...
}

So I developed a way to do that, it's pretty simple:

#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <variant>
#include <meta>
#include <ranges>

struct __attribute__((packed)) Foo {
int& abc;

~Foo() {
static_assert(sizeof(void*) == 8, "Use modern CPU.");

static_assert([]() consteval {
auto members = std::meta::nonstatic_data_members_of(
^^Foo,
std::meta::access_context::current()
);

for (auto member : members) {
auto type_info = std::meta::type_of(member);
std::size_t layout_size = std::meta::is_reference_type(type_info)
? sizeof(void*)
: std::meta::size_of(type_info);

if (layout_size != 8) {
return false;
}
}
return true;
}(), "All non-static data members of Foo must occupy 8 bytes in layout!");

if (reinterpret_cast<uintptr_t>(&abc) & 0x1) {
delete reinterpret_cast<int*>(reinterpret_cast<uintptr_t>(&abc) &
~uintptr_t(0x1));
}
}
};

int main(void) {
std::variant<int, Foo> v;
v.emplace<0>(420);
v.emplace<1>(([&]() -> int& {
int* x = new int;
*x = std::get<0>(v);
x = reinterpret_cast<int*>(reinterpret_cast<uintptr_t>(x) | 0x1);
return *x;
})());
std::cout << *reinterpret_cast<int*>(
reinterpret_cast<uintptr_t>(&std::get<1>(v).abc) &
~uintptr_t(0x1))
<< std::endl;
return 0;
}

[https://godbolt.org/z/91rcbEdG5](https://godbolt.org/z/91rcbEdG5)

It's also memory safe.

Cheers.

https://redd.it/1v58tj3
@r_cpp
muopt: a C++17 micro-library for argument parsing

Hello! I wanted to share a project that I've been working on for the last few days, a small argument parsing library called muopt. It started from a quite popular Rust crate called lexopt that I use in one of my CLI Rust projects. I wanted something similar to lexopt and couldn't find it for C++, so I decided to write my own.

muopt is more of a tokenizer/lexer than it is a parser. It doesn't "connect" a value to its option and just views them as an argument. muopt is intended to be minimal and bare-bones. Below is a really simple example of muopt in action.

#include "muopt/muopt.hpp"
#include <iostream>

int main(int argc, char argv) {
muopt::Parser parser(argc, argv);

while (std::optional<muopt::Arg> arg = parser.next()) {
if (arg->islong("help"))
std::cout << "usage: example [--input FILE]\n";
if (arg->is
long("input"))
std::cout << parser.argvalue().valueor("") << '\n';
}
}

The code above will match the arguments --help and -h. Both will print out the usage message. It will also match --input=Hello or --input Hello and print out the value. The README has a deeper explanation on how muopt works and the repo has more examples.

muopt is still very early; it's not even released yet. I'm still debating on how to take on certain things, for example the arg_value() return type and how errors should be handled. Any suggestions are welcomed!

GitHub: https://github.com/secona/muopt

https://redd.it/1v6342h
@r_cpp
Profiles and Contracts - Type/Memory Safety in C++?

With the massive push for memory and type safety right now, there is a lot of talk in the C++ community about "profiles" and "contracts." They get grouped together a lot, but they solve entirely different problems.

Simply put: Profiles act as a set of global static rules that the compiler enforces to restrict the language itself. Contracts act as local conditions (both pre and post) which enforce your own API logic.

Here's some more breakdown between the two:

Contracts let you formalize your code's assumptions with pre, post, and contract_assert. To explain the difference between the three:

`pre` refers to preconditions, these are appended to function declarations and serve to evaluate predicates before the function body executes. If that condition evaluates to false, then the contract is broken. This can lead to things such as the program crashing or logging the error.
post refers to postconditions, which are also appended to function declarations but evaluate the state of your data after the function finishes running. They act as a guarantee to whoever called your function.
`contract_assert` lets the programmer specify properties of the program's state that are expected to be held up at certain points.

Here's a code example:

#include <iostream>

// Simple function showing where the contract keywords go
int divide_numbers(int numerator, int denominator)
pre (denominator != 0) // pre: Cannot divide by zero
post (result : result <= numerator) // post: Result must be <= numerator
{
// Internal assertion: Sanity check an internal assumption
contract_assert(numerator >= 0);

return numerator / denominator;
}

int main() {
// Compiles perfectly! However, at runtime, this triggers a precondition failure
// and terminates the program before ever hitting the internal contract_assert.
int output = divide_numbers(-10, 0);
std::cout << "Result: " << output << "\n";

return 0;
}

You can read about contracts in general at: [
https://en.cppreference.com/cpp/language/contracts](https://en.cppreference.com/cpp/language/contracts)

You can read more about pre/postconditions at: [
https://en.cppreference.com/cpp/language/functions/function\_contract\_specifiers](https://en.cppreference.com/cpp/language/functions/function_contract_specifiers)

For more information, check out open std: [
https://open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2900r13.pdf](https://open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2900r13.pdf)

Now for profiles!

Profiles essentially act as a set of global static rules which enforce the standards you have written, a good example of a profile is:

&#8203;

// std::init: every object is initialized before it is read.
[profiles::enforce(std::init)];


struct Options {
    int timeoutms;
    bool verbose;
};


bool parse
flag(const char arg, Options &out);


int run(int argc, const char argv) {
    Options opts;                       // error: 'opts.timeout_ms' is indeterminate
    int retries;                        // error: uninitialized


    for (int i = 1; i < argc; ++i)
        if (parse_flag(argv[i], opts))
            retries = 3;


    return opts.timeout_ms
retries;   // error: read before initialization
}

You can read more about profiles at: https://cpp-profiles.org/

Also, do consider checking open std: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3589r2.pdf

You can even test out an example at: https://godbolt.org/z/n3TPjbqdY

So, with this information...


... What are your thoughts on contracts and profiles?

... Do you have a preference?

... Which should be implemented? Should both be implemented?

... Which one should we
No, -Wall -Werror Does Not Guarantee Detection of Conditionally Uninitialized Local Variables

There seems to be a misconception or lack of clarity about the usage of the compilation flags -Wall and -Werror and their ability to detect uninitialized stack variables. It depends. In this post, I will try to explore this further and explain when and how it is detected, and where it may not detect an uninitialized local variable and hence requires more caution while dealing with it. Consider the following code:

int main()
{
int value;
return value;
}

Compile this code as follows:

~/cpp_26_uninitialised$ g++ -std=c++23 -Wall -Werror test.cpp -o test
test.cpp: In function ‘int main()’:
test.cpp:4:12: error: ‘value’ is used uninitialized [-Werror=uninitialized]
4 | return value;
| ^~~~~
test.cpp:3:9: note: ‘value’ was declared here
3 | int value;
| ^~~~~
cc1plus: all warnings being treated as errors

So, with the use of the two compilation flags, `-Wall` and `-Werror`, the compilation fails, and you can then take corrective measures to address the uninitialized variable in the code.

Strictly speaking, -Wall enables the -Wuninitialized warning, and -Werror turns that warning into an error. However, *this is true only when the variable is uninitialized on every path to its use*, as `value` is here. It doesn't reliably work for a variable that is initialized on some paths and left uninitialized on others. Have a look at the following code:

// config_timeout_warning_test.cpp
#include <charconv>
#include <cstdio>
#include <string_view>

constexpr int default_timeout_ms = 1000;

// Reads "timeout_ms=<number>" from a device configuration line.
// The bug: when the key is missing, timeout_ms is never assigned.
[[gnu::noinline]] int read_timeout_ms(std::string_view config)
{
int timeout_ms;

constexpr std::string_view key = "timeout_ms=";

if (const auto pos = config.find(key); pos != std::string_view::npos)
{
const char* first = config.data() + pos + key.size();

const char* last = config.data() + config.size();

std::from_chars(first, last, timeout_ms);
}

return timeout_ms;
}

void open_device(const char* name, std::string_view config)
{
const int timeout_ms = read_timeout_ms(config);

if (timeout_ms <= 0)
{
std::printf("%-7s no timeout configured, using default %d ms\n", name, default_timeout_ms);
}
else
{
std::printf("%-7s timeout %d ms\n", name, timeout_ms);
}
}

int main()
{
// The empty configuration does not contain "timeout_ms=".
// Therefore, timeout_ms is returned without being initialized.
open_device("logger", "");

return 0;
}

Compile the code as follows:

~/cpp_26_uninitialised$ g++ -std=c++23 -O0 -g -Wall -Werror config_timeout_warning_test.cpp -o timeout_warning_test

The compilation succeeds without a single warning. Run Valgrind on it:

~/cpp_26_uninitialised$ valgrind --track-origins=yes ./timeout_warning_test
==2896379== Memcheck, a memory error detector
==2896379== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2896379== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==2896379== Command: ./timeout_warning_test
==2896379==
==2896379== Conditional jump or move depends on uninitialised value(s)
==2896379== at 0x401268: open_device(char const*, std::basic_string_view<char, std::char_traits<char> >) (config_timeout_warning_test.cpp:31)
==2896379== by 0x4012D3: main (config_timeout_warning_test.cpp:45)
==2896379== Uninitialised value was created by a stack allocation
==2896379== at 0x401166: read_timeout_ms(std::basic_string_view<char, std::char_traits<char> >) (config_timeout_warning_test.cpp:10)
Which API design should my framework have?

right now in vexa(my game/graphics library) how you initialize window with a renderer is rather straightforward:

```cpp
\#include <vexa/vexa.hpp>

using namespace vexa;

int main()
{
Engine::Init(Engine::VIDEO);

Window window = Window().setSize({1280, 720}).setRenderer({}).create();

// ...

```
YES, it uses builder pattern, not very C++-ish but i like it.
However, there are two problems with it.
first one is that usually you have builder struct building to target type so that you dont need something like `.create()` that creates classic `if (m_created) ...` problem a.k.a two phase construction. but bigger problem that caused me two want to switch to this API style is about memory management. and it is now C++'s problem.. Even rust cant handle it, because this is what you tell it to do.
so lets dont talk too much:

```cpp

Engine::Init(Engine::FONT); // calls `TTF_Init();` by SDL_TTF behind the scenes

Font font = Font::Load("...");
...
Engine::Quit();
```

A Font has destructor running on scope exit. which means before font object gets destroyed on '}' Engine::Quit() so TTF_Quit() runs. You see, when Font object gets unloaded at scope end the subsystem is not live anymore, we get a segfault. to fix this i've made Engine have a Guard struct which does Engine::Quit() automatically. But then decided that this is not i want for my API. I would priorotize explicitness for the master deinitialization.

to fix this im thinking of new API design style:
```cpp
Window window = Engine::BuildWindow().setSize({1280, 720}).setRenderer().create();
```

where BuildWindow() returns WindowBuilder not a Window, unlike the previous(so bad) choice.
for fonts, as you might already guess:

```cpp
Font font = Engine::LoadFont("...");
```

which creates a font and returns reference to it, resulting Engine to know about font count and Quit or not based on that.

code: https://github.com/monjaris/vexa

https://redd.it/1x8gtg7
@r_cpp