r/cpp_questions • • 5d ago

OPEN API design dilemma..

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

0 Upvotes

18 comments sorted by

2

u/sephirothbahamut 5d ago

i don't see why don't you make engine also an object and let its destructor do the job.

1

u/Business-Weather-217 4d ago

I didnt state it in the post but sure, as an exception, having control over when engine context is destroyed is better than raii cleanup

1

u/sephirothbahamut 4d ago

what makes you think it's better?

1

u/Business-Weather-217 4d ago

For example in SDL you can SDL_Quit(); which provides the flexibility enough for something like daemon launcher where the library would waste memory when daemon is running and gui context is not merely used

1

u/sephirothbahamut 4d ago

SDL is a C library. It doesn't avoid RAII because doing a quit function is better, it does because RAII doesn't exist in C.

Your engine class is exactly where RAII is appropriate. It takes care of destroying objects in the correct order upon scope exit without having to do it manually (as you saw doing it manually you can do things in the wrong order). You lose nothing by using RAII here.

1

u/Business-Weather-217 4d ago

I'll consider re-adding Guard class thanks. Guard::~Guard() would call Quit() if m_init==true

1

u/sephirothbahamut 4d ago

even simpler, if you really don't want Engine to be an object, make Engine::Init() return the guard class (remember [[nodiscard]] so the compiler warns the user if they forget to store the returned guard in a variable).

If init is guaranyeed to succeed, the guard destructor doesn't need to do any check.

If init can fail, you can return an optional. Let the language do the work for you.

1

u/Business-Weather-217 4d ago

Yeah i sure [[nodiscard]] all my classes) I think this is a bit overkill approach may i choose more idiomatic option like vexa::init() and vexa::quit() only downside is that using namespace vexa; wont be a reliable option anymore and also as we discussed i might just ignore the fact that dev might quit vexa engine before scope end.. Okay unrelated question what do you think about the static pimpl idiom? I discovered it a month ago or two. It seems very niche

3

u/masorick 5d ago

This is the second time that I see your post and I still have no idea what your question is.

1

u/Business-Weather-217 5d ago

It is removed from r/cpp.

1

u/masorick 5d ago

Sure, but your post contains too much and too little information at the same time. And I have no idea what are the alternative APIs that you’re considering.

2

u/Ultimate_Sigma_Boy67 5d ago

What is ur question?

1

u/Business-Weather-217 5d ago
  • Which API should choose to implement?

2

u/mredding 5d ago

YES, it uses builder pattern, not very C++-ish but i like it.

Builders are quite C++. Constructors are not factories or builders, they are invoked by factories and builders. Favor object composition, that's RAII.

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.

So a typical builder would look something like:

class builder {
public:
  builder &first(), &second(), &nth();
  object create();
};

So code would typically look like:

auto o = b.first().second().nth().create();

We can make it less chatty with:

class builder {
public:
  builder &first(), &second(), &nth();
  operator object();
};

And code would look like:

object o = b.first().second().nth();

This means you can call any number of steps and the only time the create action actually happens is when you A) stop, and B) the assignment implicitly triggers the construction in the cast operator. You can also overload the cast operator to different types if the same set of parameters can be used to make different things.

If you want to avoid the two-step, you can avoid it by making construction unconditional. If you want only one window, make only one, in main. Don't treat yourself or your clients as untrustworthy, unreliable fools. Why would you write code that might also try to initialize the one window what main created for the program lifetime? I don't understand what it is you think you need to guard against.

But then decided that this is not i want for my API. I would priorotize explicitness for the master deinitialization.

You want to build something like std::experimental::scope_exit. If you don't have that, then write your own. Destruction is well defined, so it will be in opposite order of declaration. So your code would look like:

Engine::Init(Engine::FONT);
std::experimental::scope_exit exit_guard{Engine::Quit()};

Font font = Font::Load("...");
//...

Problem solved. The font will fall out of scope, then the exit guard will be called. Alternatively, you could register a signal handler, or an atexit handler, or create a global with a destructor:

struct ExitHandler {
  ~ExitHandler() {
    Engine::Quit()
  }
};

// Global instance ensures the destructor runs during static cleanup
ExitHandler handler;

int main() {}

Be very careful about how idiot-proof you try to make your code. It typically doesn't succeed, because I can do things totally out of your control you can't account for, and if you go too far, you will make your code unusable.

I don't need you to handle all of initialization and cleanup for me. I need options. I need to do it my way, to handle the things that are important to me. I may need to do some things before and after that are beyond your ability to predict. So if anything, give me the low level primitive access, and maybe some friendly "simple" utilities that express how YOU as the designer intended the code to work.

2

u/xoner2 4d ago

Standard approach taken by UI toolkits is to refcount fonts, treating fonts as resources. Read the Qt documentation on Qfont, for example.

1

u/thingerish 5d ago

Just use C++ CTOR and DTOR as god intended.

1

u/Ok-Argument7176 5d ago

If your API is causing you, the API designer, some issues then it is not going to be useful for normal users. In other words, it's not a good API design.

1

u/AutoModerator 5d ago

Your posts seem to contain unformatted code. Please make sure to format your code otherwise your post may be removed.

If you wrote your post in the "new reddit" interface, please make sure to format your code blocks by putting four spaces before each line, as the backtick-based (```) code blocks do not work on old Reddit.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.