Post

C++ Multiple file project

Normally when dealing with C++ projects, developers structure their code in source files and headers. Here I want to show how to create a simple project without external dependencies. The entire example can be found in my github repository blogging-code.

File structure

Let’s design a project with two modules, think of it as if module1 was a physics module and module2 was some sort of interfacing file with that module. It is a good design pattern to isolate sub-projects inside the same project. The file strcucture I propose for the example is (when I run tree in my project root directory):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── include
│   ├── module1
│   │   ├── module1c1.hpp
│   │   └── module1c2.hpp
│   └── module2
│       ├── module2c1.hpp
│       └── module2c2.hpp
├── scripts
│   └── compile.sh
└── src
    ├── main.cpp
    ├── module1
    │   ├── module1c1.cpp
    │   └── module1c2.cpp
    └── module2
        ├── module2c1.cpp
        └── module2c2.cpp

In each cpp file we should place (in mod1c1.cpp):

1
2
3
4
5
6
7
#include <iostream>

#include <module1/module1c1.hpp>

void mod1c1::foo(){
    std::cout << "mod1c1\n";
}

where we substitute module1/mod1c1.hpp and mod1c1 depending on the filename. If for example we are working in file mod2c1 we would substitue by module2/mod2c1 and mod2c1. They are the same files but chainging the name.

Also the header files should be something like

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef INCLUDE_MOD1C1_HPP
#define INCLUDE_MOD1C1_HPP

#include <iostream>

class mod1c1{
public:
   void foo();
};

#endif

As before, for this example substitute the numbers of mod1c1 to the corresponding ones according to the filename.

Finally we add a main.cpp file that will contain the main function (entrypoing) to generate an executable that uses all the modules and functions in the src. The contents of the file main.cpp are

1
2
3
4
5
6
7
8
9
10
11
#include "module1/module1c1.hpp"
#include "module1/module1c2.hpp"
#include "module2/module2c1.hpp"
#include "module2/module2c2.hpp"

int main(){
    mod1c1 m1c1; m1c1.foo();
    mod1c1 m1c2; m1c2.foo();
    mod2c1 m2c1; m2c1.foo();
    mod2c2 m2c2; m2c2.foo();
}

Compilation and linking for executable

It is time to compile the source files to the objects, first create a directory build where we are going to store all the builds, commonly we also create subdirectories for obj to store the objects, bin to store the binaries and lib for the libraries

1
2
3
4
5
6
# recreate build
rm -rf build
mkdir build

# create subdirectories
mkdir build/obj build/bin build/lib

Now compile the source files to objects and place them in the subfolder obj:

1
2
3
4
5
6
7
8
# compile all sources
g++ -std=c++17 -Iinclude -c src/module1/module1c1.cpp -o build/obj/moudle1c1.o
g++ -std=c++17 -Iinclude -c src/module1/module1c2.cpp -o build/obj/moudle1c2.o
g++ -std=c++17 -Iinclude -c src/module2/module2c1.cpp -o build/obj/moudle2c1.o
g++ -std=c++17 -Iinclude -c src/module2/module2c2.cpp -o build/obj/moudle2c2.o

# compile main
g++ -std=c++17 -Iinclude -c src/main.cpp -o build/obj/main.o

We are choosing the C++ standard with the flag -std (check the versions in cpprefenence.com). The include directory is “included” with the flag -I and according to the project structure is include. Finally the flag -c is to tell the compiler to compile the source into an object file. For faster compilation use additional flags like -O3 (aggressive optimization, you can do O2 or O1 for less optimization), -march=native (optimizing for your specific CPU), -flto (link time optimization for cross file optimizations) and -ffast-math (aggresive floating point optimizations).

After the sources have been compiled and we checked that there is no error the next step is to link the objects. The linker will check all the definitions of the objects and generate a main executable that when run will start with the int main() function. The function main() could be in any of the object files but the important is that when liking several objects there should be only one main function to generate an executable. Let’s link all the objects with the command

1
2
3
4
5
6
7
# link all the objects
g++ build/obj/moudle1c1.o \
    build/obj/moudle1c2.o \
    build/obj/moudle2c1.o \
    build/obj/moudle2c2.o \
    build/obj/main.o \
    -o build/bin/main

See that we placed the executable in build/bin/main, execute the binary as

1
./build/bin/main

For convenience and make things faster I like to create scripts. Place this bash script in scripts/compile.sh with the content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
THIS_DIR=$(dirname "$(realpath "$0")")
ROOT_DIR=$(dirname ${THIS_DIR})

recreate_dirs(){
    # removing build directory
    echo "Removing ${ROOT_DIR}/build and recreating..."
    rm -rf ${ROOT_DIR}/build
    mkdir ${ROOT_DIR}/build

    # creating directories for the build
    mkdir ${ROOT_DIR}/build/obj
    mkdir ${ROOT_DIR}/build/bin
    mkdir ${ROOT_DIR}/build/lib
}

compile_exec(){
    recreate_dirs

    # compile to objects
    echo "Compiling objects for executable..."
    g++ -std=c++17 -Iinclude -c src/module1/module1c1.cpp -o ${ROOT_DIR}/build/obj/moudle1c1.o
    g++ -std=c++17 -Iinclude -c src/module1/module1c2.cpp -o ${ROOT_DIR}/build/obj/moudle1c2.o
    g++ -std=c++17 -Iinclude -c src/module2/module2c1.cpp -o ${ROOT_DIR}/build/obj/moudle2c1.o
    g++ -std=c++17 -Iinclude -c src/module2/module2c2.cpp -o ${ROOT_DIR}/build/obj/moudle2c2.o

    # compile the main to object
    g++ -std=c++17 -Iinclude -c src/main.cpp -o ${ROOT_DIR}/build/obj/main.o

    # link all the objects
    g++ ${ROOT_DIR}/build/obj/moudle1c1.o \
        ${ROOT_DIR}/build/obj/moudle1c2.o \
        ${ROOT_DIR}/build/obj/moudle2c1.o \
        ${ROOT_DIR}/build/obj/moudle2c2.o \
        ${ROOT_DIR}/build/obj/main.o \
        -o ${ROOT_DIR}/build/bin/main
}


croak(){
  echo "[ERROR] $*" > /dev/stderr
  exit 1
}

main(){

  if [[ -z "$TASK" ]]; then
    croak "No TASK specified."
  fi
  echo "[INFO] running $TASK $*"
  $TASK "$@"
}

main "$@"

To execute it you just need to run

1
2
3
4
5
# make the script executable
chmod +x scripts/compile.sh

export TASK=compile_exec
./scripts/compile.sh

The new generated executable will be found in build/bin/main, execute it as:

1
./build/bin/main

To get the prints of each function on your screen.

Remarks

What we have shown here is the bare bones of a C++ project. We created an executable that runs several functions defined in different files.

This is the basics, in follow up posts we will learn how to compile code using Makefile and cmake, better and more convenient tools to compile code. We will also learn to create static and dynamic libraries of our code to be used in other projects and also how to compile and link external libraries like OpenCV, Boost, Eigen or cBLAS.

This post is licensed under CC BY 4.0 by the author.