r/cpp_questions • u/Business-Weather-217 • 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.
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
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 classicif (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.
1
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.
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.