Seminar: Compilation and makefiles

Compiling in C++

Compilation in C++ is performed by a compiler. If you do not remember what compilers do, go back to the previous module, Topic 4 of this course (the Introduction to C++).

If you followed the instructions on the welcome page you should have a compiler installed.

This is g++ and it is a command-line utility with various options, some of which depend on your specific machine.

When your code is contained in a single source file, e.g. main.cpp compilation is trivial

g++ main.cpp -o myprogram

This produces an executable file named myprogram that you can run via

./myprogram

Compilation flags

The g++ compiler had various compilation flags that go under the following syntax

g++ [options] source_files -o output_file

Common flags include:

  1. Optimization:
    • -O0: No optimization (default).
    • -O1, -O2, -O3,-Ofast: Increasing levels of optimization.
  2. Debugging:
    • -g: Include debugging information for use with a debugger like gdb.
  3. Warnings:
    • -Wall: Enable common warnings.
    • -Werror: Treat warnings as errors.
  4. Standards Compliance:
    • -std=c++11, -std=c++14, -std=c++17, etc.: Specify the C++ standard to follow.
  5. Linking:
    • -L<path>: Specify a directory for library search paths.
    • -l<library>: Link a specific library (e.g., -lm for the math library).
  6. Output:
    • -o <filename>: Specify the output file name.
  1. Preprocessor:
    • -D<macro>: Define a preprocessor macro.
    • -I<path>: Add include directory for header files.
  2. Performance:
    • -fopenmp: Enable OpenMP for parallel programming.

For example, the following compiles two files main.cpp and utils.cpp to produce a single program program with optimisation level 2, with warnings enabled and using the 2017 standard for C++.

g++ -std=c++17 -O2 -Wall -o program main.cpp utils.cpp

Notice that this in fact performs two actions at the same time:

  • compiles the source code into objects, equivalent to
g++ -std=c++17 -O2 -Wall -c main.cpp  -o main.o 
g++ -std=c++17 -O2 -Wall -c utils.cpp -o utils.o
  • links the object into an executable
g++ -std=c++17 -O2 -Wall main.o utils.o -o program

Makefiles

When a project becomes complex (with many files and eventually various kinds of flags) typing a long string just for compilation makes little sense. Ideally, we want to automatise this step, by writing a dedicated script that does it for us.

We could write a separate bash script to do so, but it would not be very efficient.


The standard way is to construct a Makefile and to use it with the normally available command-line utility make. Makefiles are a special type of file with a characteristic syntax.

Often, Makefiles are quite complex, or even automatically generated. Here, we write a simple, interpretable makefile from scratch.

Creating a Makefile

We want to transpose the compilation commands into a makefile. So, in the source folder (i.e. wherever the code is stored) we create a file called Makefile.

touch Makefile

Structure of a Makefile

A Makefile consists of

  1. Variables: these identify compilers, file names, and flags.
  2. Rules: these are actions that the Makefile can perform, each with a name associated with it. Each rule consists of a target, dependencies, and a recipe (command).

Variables

The obvious variables for a Makefile are:

  • the compiler name. Typically this variable is called CC.
  • the compiler flags. Typically this variable is called CFLAGS.
  • the source files, i.e. the source code for our program. This is normally called SRC.
  • the executable name, e.g. EXEC.

Note that typically these names are written in uppercase letters. In the Makefile syntax, we access the value stored in these variables using the $(VARIABLE) syntax.

Rules

Rules in a Makefile describe how to build a target from its dependencies using commands. A rule typically follows this format:

target: dependencies
    commands
  • target: The file or outcome to create (e.g., an object file .o or the final executable).
  • dependencies: The files or other targets required to build the target.
  • commands: The shell commands to execute, usually for compiling or linking.

Minimal makefile

Suppose that we want to write a rule to compile our project. Our target would be $(EXEC) (the name of our executable) our dependencies would be the source code $(SRC) and the command would simply combine the compiler, its flags, the source code and the executable name, so that the rule would look like

$(EXEC): $(SRC)
    $(CC) $(CFLAGS) $(SRC) -o $(EXEC)

A complete minimal Makefile would look like (remember to use true tabs for the indentation)

CC = g++
CFLAGS = -O2 -Wall
SRC = main.cpp utils.cpp
EXEC = program

# check that the indentation is a tab and not spaces
$(EXEC): $(SRC)
    $(CC) $(CFLAGS) $(SRC) -o $(EXEC)

Running a makefile

How would we run this Makefile? You should type make and the name of the rule that you want to call, in this case program as in

make program

What if we just want to have a default rule to run every time we type make (after all, we want to simplify our lives)? There is a special target named all. We can then rewrite our minimal Makefile as

CC = g++
CFLAGS = -O2 -Wall
SRC = main.cpp utils.cpp
EXEC = program

# check that the indentation is a tab and not spaces
all: 
    $(CC) $(CFLAGS) $(SRC) -o $(EXEC)

Keeping all as the first rule in the Makefile makes it default.

Another useful rule is called clean: you can implement it to remove files, as to remove the executable safely.

clean:
    rm -f $(EXEC)

Task 1: writing a minimal Makefile

Using the information above, download the following project folder and write a minimal Makefile that can compile the following project:

🗜️ makefiles.zip

Run your makefile using the make command.

Task 2: adding compilation flags

Modify your Makefile to use the -std=c++17 compilation flag.

Task 3: separating compilation and linking

The Makefile you have written is minimal and blends together compiling and linking. However, it is a better practice to separate the two, since very large projects can have many files and modifying only a few of the object files instead of recreating all of them is much more efficient.

The Make syntax has a convenient set of features that allow you to automatically produce a list of object files. For example

OBJ = $(SRC:.cpp=.o)  # Converts .cpp files to .o files

creates a variable called OBJ that contains all the .o object files such as main.o produced by g++ -c main.cpp -o main.o.

Do the following:

  1. use OBJ = $(SRC:.cpp=.o) to define your object files. You can print in makefiles using the command @echo, so that @echo $(OBJ) should print to terminal the values stored in OBJ. Use the fact that rules can be multiline to modify your Makefile and print your object variable.
  2. Now, you could write a rule for every source file to be converted into an object file, but the make syntax has a universal pattern rule for that:
%.o: %.cpp
    $(CC) $(CFLAGS) -c $< -o $@

which tells make to compile each .cpp file ($<) into the corresponding .o file ($@). Add this rule to your makefile.

  1. Now you have a rule that creates object files, so you only need a rule to link them to produce the executable. Write it down, noticing that your target is $(EXEC), your dependencies are now the object files and the rule is just the linking step described above.
  2. Now, modify your all rule to depend on $(EXEC) and to write the message "Build complete". The all rule should do nothing but printing messages.
  3. Finish by improving your clean rule to also clean from the object files.