5

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();
}
1
  • 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 ;-) ). Commented Apr 8 at 18:23

4 Answers 4

6

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 to app->run() or app->execute().
  • The run() or execute() 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:

2
  • 2
    If 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
    Commented Apr 8 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. Commented Apr 8 at 14:42
4

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.

1
  • 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
    Commented Apr 8 at 14:52
4

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.

3
  • 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 RAII
    – Konjointed
    Commented Apr 8 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
    Commented Apr 8 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.
    Commented Apr 8 at 17:49
0

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.

3
  • 1
    Singleton is an anti-pattern.
    – Pablo H
    Commented Apr 8 at 15:03
  • 4
    @PabloH: the term anti-pattern is heavily overstated.
    – Doc Brown
    Commented Apr 8 at 15:36
  • 3
    A 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. Commented Apr 8 at 19:55

Not the answer you're looking for? Browse other questions tagged or ask your own question.