Using compiler and build system ---------------------------------- Transferring the source code into an executable binary is the task of the compiler, which in our case will be `g++` from the GNU Compiler Collection (GCC). First simple program ^^^^^^^^^^^^^^^^^^^^^^^^ To get started, write a simple program like the following that prints out its first command line argument. .. code-block:: c++ :linenos: #include #include int main(int argc, char *argv[]) { // if the number of given command line arguments is only 1 (= the program name), print out usage information and exit if (argc == 1) { std::cout << "usage: " << argv[0] << " " << std::endl; return EXIT_FAILURE; } // read in the first argument std::string filename = argv[1]; // print message std::cout << "Filename: \"" << filename << "\"" << std::endl; return EXIT_SUCCESS; } Note what the program does. In line 7 the number of arguments to the program is checked. The value of 1 means we have no arguments, only the program name that we always get. In this case, we print a message and exit. In line 15 we have at least one argument and store it in a string and print it out. Compile the program to produce a binary called `test` with the command .. code-block:: bash g++ main.cpp -o test and verify that it works as expected: .. code-block:: bash $ ./test usage: ./test $ ./test input.txt Filename: "input.txt" .. _multiple_source_files: Multiple source files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Next, we add a function to parse the given file, which will contain the parameters for our simulation later. In order to keep the code well organized, we add a `*.h` header file, `settings.h` with the *declaration* of the new function: .. code-block:: c++ :linenos: #include //! parse a txt file with settings, each line contains " = " void loadFromFile(std::string filename); The corresponding `*.cpp` file, `settings.cpp`, contains the *definition* of the function: .. code-block:: c++ :linenos: #include // for file operations #include // for cout void loadFromFile(std::string filename) { // open file std::ifstream file(filename.c_str(), std::ios::in); // check if file is open if (!file.is_open()) { std::cout << "Could not open parameter file \"" << filename << "\"." << std::endl; return; } // loop over lines of file for (int lineNo = 0;; lineNo++) { // read line std::string line; getline(file, line); // at the end of the file break for loop if (file.eof()) break; // print line std::cout << "line " << lineNo << ": " << line << std::endl; } } Again, take a closer look at the code to understand what happens. The file will be read line by line in the for loop at line 17, in line 28 we simply print the currently parsed line. In the already existing `main.cpp` source file, add `#include "settings.h"` at the very top and the call to the new function, .. code-block:: c++ :linenos: loadFromFile(filename); at the proper position at the end of the main function. Compilation of all files is done by .. code-block:: bash g++ main.cpp settings.cpp -o test2 Note, that only the source files with `*.cpp` suffix need to be given to GCC. The order does not matter. In this example, the compiler did actually two steps. In the first step, it compiled all (two) compilation units separately. A compilation unit consists of a source file with ending .cpp. Besides the contained source code it may call functions of other compilation units. For this it needs to know only their *signature* which is given by the *declaration* in the included header file. The second step is the linking step, where the linker connects all compiled compilation units as well as external libraries to form a single executable binary. In the above example, both steps together were performed by a single command, but it is also possible and common to do these steps separately. The compilation of source files is independent of each other and thus could be done in parallel. In projects with a lot of files, this can vastly speed up the build process. Only the linking step has to be done afterwards. Now the purpose of header files becomes clear. First, it is a way to tell the compiler which functions (and classes, structs or global variables) are available in other compilation units. Second, it is nice for the programmer to see these compact collections of functions without the detail of their actual implementation. Therefore, header files usually contain good comments that specify what the functions do and in what context they could be used. Source files almost always appear as a pair of `file.cpp` and `file.h` (except for `main.cpp` that doesn't need any header file because it is not included by anything else). To validate the compiled program, create the following test file, `parameters.txt` (it ends with an empty line): .. code-block:: bash # settings endTime=1.5 # duration of the simulation re = 1000 # Reynolds number and test your program: .. code-block:: bash $ ./test2 parameters.txt Filename: "parameters.txt" line 0: # settings line 1: endTime=1.5 # duration of the simulation line 2: line 3: re = 1000 # Reynolds number line 4: Advanced compiler usage ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There exist more compiler options that are relevant. The compiler can try to produce more efficient binary code than the naive one that results from directly transferring each C++ statement to machine code. The optimization level of the compiler can be set to `-O0` (disable optimizations, reduce compile time) over `-O1`, `-O2` to `-O3` (optimize the most, but sometimes `-O2` is faster). Ultimately, there is also the `-Ofast` level, where it is allowed to alter the numerical results by small rounding errors, e.g. by applying the associative law to additions and multiplications or assuming there will be no division by zero, etc. (interested readers can look at the `full documentation `_). Usually when software is developed, there exists a `debug` target without any optimization that is used during development and a `release` target with optimization that produces an efficient program. Code compiled for a `release` target often produces less debugging output. In order to differentiate the two targets in the code, the constant NDEBUG is defined in the release target by adding `-DNDEBUG` at the compiler command. Conditional output in the code can take the following form: .. code-block:: c++ :linenos: #ifndef NDEBUG // only run this code in debug target std::cout << "lots of inefficient but informative output . . ."; #endif (Note the double negation in `#ifndef NDEBUG` which looks awkward, but is commonly used.) The release target compile command, for the previous test program, can look as follows. .. code-block:: bash g++ settings.cpp main.cpp -Ofast -DNDEBUG -o test2_release You won't notice any differences in runtime here, because there is not enough computation going on in this example. For the final submission of exercise 1, there will be a significant difference, though, e.g. a factor 12 between -O0 and -O3 in the reference implementation. Further compiler options deal with warnings (starting with `-W...`) or debugging (starting with `-g`) among others. See the `GCC documentation `_ for details. One more frequently needed functionality is to link to external libraries. Libraries contain compiled code that can be accessed from a C++ program. E.g. to link to the library `libvtk`, which implements data output to files that can be visualized with the tool ParaView, use the option `-lvtk`. Makefiles ^^^^^^^^^^^^^^^^^^ Handling different compiler commands for different targets can be facilitated by using `make`. This is a Linux program that executes commands given in a `Makefile`. The commands can be organized by a target name. E.g., create the following file with name `Makefile`: .. code-block:: makefile :linenos: all: debug release debug: g++ settings.cpp main.cpp -g -o test2 release: g++ settings.cpp main.cpp -Ofast -DNDEBUG -o test2_release clean: rm test2 test2_release Note, the indenting characters have to be *tabs*, not spaces. If you now execute .. code-block:: bash make debug the command in line 4 will be executed, which builds the debug version of the test program. Analogously, `make release` builds the release target given in line 7. A clean target, `make clean`, is usually specified to delete all previously built files. If you specify no target, i.e. simply call `make`, the first target will be executed, in this case it has the same effect as `make all`. This will build both the `debug` and the `release` target. To avoid repetitions in the Makefiles, variables can be used, e.g. for source files and compiler flags. The following Makefile uses variables and has the same effect as the previous version, except for the additional `-Wall` flag, which enables more useful compiler warnings. .. code-block:: bash :linenos: CXX=g++ # C++ compiler CXXFLAGS=-Wall # compiler flags # source files SRC=settings.cpp main.cpp all: debug release debug: $(CXX) $(SRC) $(CXXFLAGS) -g -o test2 release: $(CXX) $(SRC) $(CXXFLAGS) -Ofast -DNDEBUG -o test2_release clean: rm test2 test2_release Makefiles are very powerful and there is much more to learn about it, which is beyond the scope of this exercise. (E.g. try `this tutorial `_). But with the basics presented here, it is already possible to conviently use the compiler for small projects.