Compilation and Execution
- Review the stages of compilation
- Describe the platforms used in this course
- Introduce constant expressions and static assertions
"Prefer the compiler to the pre-processor." Scott Meyers (2005)
Program compilation is a multi-stage process. The initial stage collates the source code for compilation into translation units. It inserts into the implementation file the source code of all the header files referred to by the implementation file. The intermediate stage compiles the translation units individually. The final stage creates a single binary executable from all the compiled translation units. The intermediate stage uses the language's type system to evaluate the source code for syntactic errors and to construct the binary instructions that form the executable. A user can run the executable multiple times without recompiling the source code.
This chapter reviews the stages of the C++ compilation process, describes the platforms used in this course and introduces the syntax for communicating with the operating system. This chapter also describes the syntax for expressions that the compiler itself can evaluate directly and the syntax for custom checking assumptions and reporting breaches of those assumptions at compile-time.
Compilation Process
The source code for a typical C++ application may be distributed across many modules. A module's header file (*.h
) exposes its names to other modules. Its implementation file (*.cpp
) contains the definitions associated with declarations in the header files.
Transforming the original source code for an application into a binary executable involves three distinct stages:
- Pre-Processing Stage - the pre-processor creates a separate translation unit from the original source files for each module by inserting the header files into the implementation file and replacing or expanding any macros (lines starting with
#
) - Compilation Stage - the compiler creates a separate binary file from each translation unit
- Linking Stage - the linker creates a single relocatable file from the binary files for all translation units and the binary files for any referenced libraries.
To run the executable, the user instructs the operating system to load the relocatable file into memory. The loader copies the relocatable file into RAM, arranges the storage locations for the data and the code and transfers control to the entry point of the executable (the main()
function).
After the initial build, only those translation units that have been changed need to be re-compiled.
Platforms and Compilers
The platform used in this course is Windows and the development compiler is that shipped with Visual Studio.
Microsoft Visual Studio - cl
Visual Studio is an Integrated Development Environment (IDE) supported by Microsoft. Instructions for installing this IDE on a Windows platform are listed in the resources section of the course web site.
Compiler Options
To list the options available on this compiler, open a Visual Studio command-prompt window and enter
cl /?
Notable options are
/c
compile only without a link stage - create a.obj
binary file./E
pre-process only - send the output tostdout
./P
pre-process only - send the output to*.i
file./Wall
enable all warnings.
To disable warning C4996 (for example, strcpy
is unsafe - use strcpy_s
), add the following macro definition before the #include
for the 'unsafe' function:
#define _CRT_SECURE_NO_WARNINGS
Linux - gcc
The GNU Compiler Collection (gcc) is a comprehensive open-source collection of compilers available on all Linux based platforms. The version for C++ source code is called g++
. To access this compiler remotely from a Windows machine, we open a terminator emulator. Instructions for configuring an ssh terminal emulator are listed in the resources section.
To access version 9.1.0 of the GCC compiler on matrix, enter
export LD_LIBRARY_PATH=/usr/local/gcc/9.1.0/lib64:$LD_LIBRARY_PATH
/usr/local/gcc/9.1.0/bin/g++ -std=c++17 mySource.cpp
Compiler Options
To list the options available on the GCC compiler, enter
/usr/local/gcc/9.1.0/bin/g++ --help
Notable options are
-c
pre-process and compile without a link stage - create a .o binary file.-E
pre-process only - send the output tostdout
.-g
produce information for the debugger (gdb
).
Interface with the Operating System
The main()
function of a C++ program is its entry point to its executable version. This function's return type is an int
. This function accepts as its parameters either no information or a set of command-line arguments. The corresponding prototypes are:
int main(); // no command-line arguments
int main(int argc, char *argv[]); // two command-line arguments
Command-Line Arguments
The first parameter (argc
) in the command-line-arguments version of the main()
function receives the number of arguments supplied on the command line from the operating system. This number includes the name of the relocatable file. The second parameter (argv
) receives the address of an array of pointers to C-style null-terminated strings. Each pointer holds the address of a string that holds one command-line argument. argv[0]
holds the address of the name of the relocatable file. argv[i]
holds the address of the C-style null-terminated string that holds the i
-th command-line argument.
Consider the following command-line instruction:
my_prg Assignments Workshops Tests Exam
The source code listed below
// Interfacing with the Host Platform
// my_prg.cpp
#include <iostream>
int main (int argc, char *argv[])
{
int i;
std::cout << "Application: " << argv[0] << std::endl;
for (i = 1; i < argc; i++)
std::cout << "- " << argv[i] << std::endl;
}
produces the output
Application: my_prg
- Assignments
- Workshops
- Test
- Exam
On some operating systems the first argument includes the absolute path to the relocatable.
Returning Control
Upon completion of the program's execution, the main()
function returns control to the operating system. This function's return value is that of the expression on its return
statement. If this statement is missing, main()
returns the value 0
to the operating system:
int main()
{
return 0; // inserted by the C++ compiler if the return statement is missing
}
Compile-time Evaluations
Modern compilers are sophisticated enough to perform calculations that will not change during the execution of a program and store the results of those calculations in the executable code. C++ provides specialized syntax for evaluations at compile-time and for reporting custom error messages during the translation unit compilation stage.
Constant Expressions
The constexpr
keyword declares that the value of its identifier is a run-time constant and can be evaluated at compile time.
Example
In this example, the factorial calculation expressed in the form of a recursive function (a function that calls itself) does not depend on the rest of the program and is identified as a constant expression. An constant expression can only refer to variables that are also constant expressions:
// Compiler-Evaluated Expressions
// constexpr.cpp
#include <iostream>
constexpr int N = 8; // constant variable
constexpr int factorial(int i) // constant function
{
return i > 1 ? i * factorial(i - 1) : 1;
}
int main()
{
std::cout << N << "! = " << factorial(N) << std::endl;
}
8! = 40320
Both N
and the function factorial()
are evaluated at compile-time and the result is stored as a constant value in the output stream expression.
Static Assertions
The C++ language supports messaging at compile time prompted by custom checks in addition to messaging prompted by inconsistencies in the source code caught by the C++ type system.
These custom programmer-inserted checks are called assertions. The static_assert()
mechanism generates a custom compiler error message if the specified condition is not met.
static_assert(bool condition, const char* message);
In the example below, the main()
function checks the value of N
(which has been specified earlier in the translation stream) and reports an error if the value is outside practical bounds:
// Static Assertion
// static_assert.cpp
#include <iostream>
constexpr int N = 0;
constexpr int factorial(int i)
{
return i > 1 ? i * factorial(i - 1) : 1;
}
int main()
{
static_assert (N > 0, "N <= 0");
static_assert (N < 20, "N >= 20");
std::cout << N << "! = " << factorial(N) << std::endl;
}
N <= 0
static assertion failed with "N <= 0"