I'm making a game using C++ and a handful of libraries like SDL2 and OpenGL. For a lot of these libraries, you need to initialize or set them up, and there's usually some important object(s) that come out of this. For example, in SDL it's SDL_Window
and SDL_GLContext
.
Well, there's some confusion. You have this initialization code that only needs to be run once, but then you get these objects which need to be accessible throughout the program. It doesn't make much sense to make a class that just initializes a library so perhaps a middle ground would be an initialization function that puts those important objects into some global application context.
Here's a rough code example to demonstrate the two options I'm talking about which one of these would be I guess better/more typical?
class SDLInitializer {
public:
SDLInitializer() {
// init sdl
}
~SDLInitializer() {
// clean-up
}
SDL_Window* getWindow() {};
SDL_GLContext glContext getContext() {};
private:
SDL_Window* window;
SDL_GLContext glContext;
}
int main() {
SDLInitializer sdl;
}
struct AppContext {
SDL_Window* window;
SDL_GLContext glContext;
};
int main() {
AppContext ctx;
ctx.window = SDLCreateWindow();
ctx.glContext = SDLCreateContext();
}
-
Note that you may want to encapsulate these "important objects" in a class accessed through an interface; "normal code" should arguably not see them. That will make it easier to deal with changes in future versions of that library, or indeed a wholesale library switch (say, to DirectX). This implies putting the initialization into a library (and recreates the problem for that library ;-) ).Peter - Reinstate Monica– Peter - Reinstate Monica04/08/2024 18:23:02Commented Apr 8, 2024 at 18:23
4 Answers 4
More generally what you need is a Composition Root and Dependency Injection.
The Composition Root of your application can be a class or a single function. The intention of the composition root is to create and configure the various objects your system requires and constructs a tree-like structure of objects representing your application. Global variables can be avoided by passing references via constructor injection (pass references when invoking a constructor).
The combination of a Composition Root and Dependency Injection achieves what you are looking for. You get one place to configure and initialize this stuff (the Composition Root), and you can control how many copies of something get created, and who has access to what using Dependency Injection.
You can use int main()
as the Composition Root, or you can define a class, which gets initialized and invoked from main()
. There is no general guidance here. I like to start with main()
for initialization code, until the application becomes sufficiently complex that a class representing "the application" becomes a useful tool for organizing logic.
Using main()
without a class thus has these responsibilities:
- Read environmental information necessary to initialize the application, be it config files, environment variables, or command line arguments.
- Decide which of these sources take precedence in cases where a setting is detected multiple ways. For example, if an environment variable exists, but a command line argument for the same thing also exists, which does your application use?
- Configuration logic.
- Initialization logic.
- Composing the object dependency graph (the Composition Root).
That is a lot of things for one function, however if each of these things is simple, then there is nothing wrong with keeping everything in main()
. At some point, though, this becomes cumbersome to read and reason about. That's when you should consider refactoring your application startup logic into classes. Many applications end up with a structure similar to this:
int main(args)
- Read environmental information necessary to initialize the application, be it config files, environment variables, or command line arguments.
- Decide which of these sources take precedence in cases where a setting is detected multiple ways. For example, if an environment variable exists, but a command line argument for the same thing also exists, which does your application use?
- An application class
- Configuration logic.
- Initialization logic.
- Composing the object dependency graph (the Composition Root).
- Coordination logic in
main()
to read environmental information and initialize the application class, following by a single call toapp->run()
orapp->execute()
. - The
run()
orexecute()
methods would have your main game loop constituting a running, fully initialized and configured application.
There is no single best way. As usual, "best" is largely a judgement call based on the size and complexity of the application coupled with the human being's ability to reason about the system and safely make changes.
Some additional reading you might find useful:
- Dependency Composition
- What is a composition root in the context of dependency injection? over at StackOverflow
- What is Constructor Injection, and why should you use it in Spring
- This might be about Java and the Spring framework, but it provides a good general summary applicable to any language.
- Dependency Injection
- Inversion of Control
-
2If I'm understanding this correctly "composition root" boils down to some point in code where all the required objects and dependencies for the startup of a application happen? And this can either be the main function or a class or classes.Konjointed– Konjointed04/08/2024 14:08:04Commented Apr 8, 2024 at 14:08
-
2@Konjointed its some place as high up the call stack as you can get that is called only once. If you have access to main then you can put it there. Some frameworks won't give you that access so you put it as high up as you can.candied_orange– candied_orange04/08/2024 14:42:13Commented Apr 8, 2024 at 14:42
The advantage of a class is that you don't have to put anything in a global context. Instead you hand out multiple references to a single object that knows if these dependencies have been initiated and where to find them. No need for globals. This way, knowledge of where to find this stuff isn't scattered all over. You can change it in one place.
Dependency Injection is a way to do this. You can use tools like Inversion of Control Containers but DI works fine when done manually. Mark Seemann called that Pure DI. It's an old idea. Back in the day we just called it reference passing.
-
A dedicated function has same visibility rules as class. OP has made an error of exposing writable fields, but those could be made properly read-only for other compilation units.Basilevs– Basilevs04/08/2024 14:52:10Commented Apr 8, 2024 at 14:52
It doesn't make much sense to make a class that just initializes a library
Says - who?
For me, it makes perfect sense, especially when you need a combination of code + data which belongs together. Your class SDLInitializer
looks pretty good to me. If I want to find the code for initializing window or glContext, I know immediately where to look for. However, I would probably change the class name to the name from your second example, AppContext
, or maybe SDLContext
, because SDLInitializer
does not tell me that there is more than just the initializing code, but also the contextual information itself.
An AppContext
object is an object which
- knows it consists of two other elements (SDL_Window and SDL_GLContext, like your current
struct AppContext
) - knows how to initialize it's data
- knows how to cleanup it's data
- provides a proper location where the initialization code can be refactored to smaller private functions. It will stay clear where these functions belong to
- makes it easy to write code following the C++ RAII idiom.
Placing functions like SDLCreateWindow() or SDLCreateContext() somewhere else, outside a class, would only make sense to me when you expect to reuse them decoupled from a struct / class AppContext
, which I guess will not be the case.
Don't hesitate to create a new class just because you think it might be "too small" - the problem with most real-world classes is not that they are too small, but that they are too big.
Also, don't hesitate to create a new class just because there will only be one object of it. In C++, creating a class for this purpose is necessary and pretty standard. If you really think this could become error-prone because someone could try to instantiate two AppContext
objects within one application, then alas, implement the class as a Singleton, as suggested by Ash. In reality, I think this might be overcautious, overdesigned and could raise doubts by people who follow dogmatically the "Singleton is anti pattern" mantra.
-
Well my thought process (and this likely due to lack of experience/skill) was that you make a class to reuse something or make multiple of something, but in this case it's just code that's run once so it seemed a little unnecessary to have it in a class since it would just be a constructor and destructor. Thinking about it though I suppose there are benefits like RAIIKonjointed– Konjointed04/08/2024 14:27:51Commented Apr 8, 2024 at 14:27
-
1@Konjointed: there are programming languages like JavaScript or Scheme where you can create single objects without classes. In C++, however, you need to create a class, even if you only need just one object of this class. Hence, in C++, I would still not hesitate to use a class for this case.Doc Brown– Doc Brown04/08/2024 14:32:06Commented Apr 8, 2024 at 14:32
-
1+1 for RAII. A function might work if it's fire-and-forget, but as soon as you have any cleanup or resource tracking you need to do, your life becomes so much easier in C++ if you use a class and RAII - to the point it's probably worth starting with a class even if you don't (yet) need it.R.M.– R.M.04/08/2024 17:49:59Commented Apr 8, 2024 at 17:49
You'd want to use the Singleton Design Pattern
https://stackoverflow.com/questions/1008019/how-do-you-implement-the-singleton-design-pattern
It will let you run your constructor once and then have a unique initialised instance be accessible anywhere in your program.
-
1
-
4@PabloH: the term anti-pattern is heavily overstated.Doc Brown– Doc Brown04/08/2024 15:36:20Commented Apr 8, 2024 at 15:36
-
3A pattern having a name doesn't make it good thing. It makes it a recognizable thing. Singletons got a bad rep partly because that was misunderstood. Mostly they got it because people wanted to show off their DI toys that let them avoid singletons. They work as well as they ever did. How you feel about globally accessing resources through a static method is really up to you.candied_orange– candied_orange04/08/2024 19:55:47Commented Apr 8, 2024 at 19:55