DEV Community

Jonah Goldsmith
Jonah Goldsmith

Posted on • Originally published at jonahmgoldsmith.hashnode.dev on

Project Design in C/C++

Design

What do I mean by project design? What I mean is the rules that you follow while developing your projects. This could be syntax rules, header include rules, directory structure rules, and anything else that you want to keep consistent while developing. When projects start to get bigger it can get harder to keep a code base clean. Today I am going to give some example rules that I follow to solve this issue.

Header Files

One rule that I always follow is that header files should not include other header files! I know this sounds like an impossible task... but it is fairly easy and comes with amazing benefits! First here is an example of breaking the rule:

#pragma once

#incude <stdint.h>
#include "my_graphics.h"
#include "my_math.h"
#include "my_huge_header.h"

Enter fullscreen mode Exit fullscreen mode

So in this example, I have a header file that includes 4 other files. When this header gets included in any other file it will bring these 4 files with it. This may not sound like a huge problem but what if "my_huge_header.h" contained every single header file in the C standard library? Now every file is including that even if it doesn't need it. Not allowing headers to include other headers solves this issue and speeds up compile times drastically in big projects.

Here is an example of what I do instead:

#pragma once

struct object_i_need;

void create_object(object_i_need* p_obj);

Enter fullscreen mode Exit fullscreen mode

Here I have a header file that includes no other header files and forward declares a struct that I will need to use in my function. In the implementation file, I will include the header file with the definition of this struct. Another benefit that comes with this structure is that it is very easy to see what this header file is doing. All forward declarations of needed objects come first, then function prototypes.

Namespaces, and API Interfaces

Another rule I follow is keeping the majority of my functions in structs holding function pointers. This gives me a kind of "namespace" in C and allows me to keep the global namespace very light, and allows easy creation of things like a plugin system.

Here is an example:

#pragma once

struct object_i_need;

struct object_creation_api
{
    void (*create_object)(object_i_need* obj);
};

//Can use conditional compilation so projects can easily reference this api!
//Or let users extern the structs themselves.

#ifdef LINKS_TO_ME
extern struct object_creation_api* obj_api;
#endif

Enter fullscreen mode Exit fullscreen mode

In my implementation file I could do something like this:

#include "obj_creation.h"
#include "obj_i_need.h"

void internal_create_object(struct obj_i_need* obj)
{
    //Create the object
};

//Create the static instance holding the function pointers
struct object_creation_api api = {
    .create_object = internal_create_object
};

struct object_creation_api* obj_api = &api;

Enter fullscreen mode Exit fullscreen mode

Now any file that externs the struct "obj_api" can use its interface!

Recap and Rule Breaks

  • Header files cannot include other header files

  • Most functions inside of structs that hold function pointers

  • I break the first rule only for macros, platform detection, and whatever I know the whole project will need.

So these are just a few things I do to keep my codebase clean and efficient while it grows. In the next few blogs I will be going over my memory tracking strategies and my plugin system :). Thanks for reading! NOW GO CODE!

Top comments (0)