01 - intro

The preprocessor is a build process mechanism that is invoked before compilation takes place.

Preprocessor code consists of directives. A directive is a single command for the preprocessor. All directives start with # and span until end of line (not until ; - it's not regular C or C++ code). \ Can be used at the end of line to extend the directive 1 more line.

All preprocessor directives (more details in subsequent lessons):

  • # - does nothing

  • #include - pastes the content of mentioned file in place of the directive

  • #define and #undef - define and undefine identifiers for further preprocessing or text replacement (often called macros)

  • #if, #ifdef, #ifndef, #elif, (since C++23 #elifdef, #elifndef) - conditionally parse enclosed code (may contain nested preprocessor directives)

  • #error - force an error during preprocessing

  • #pragma - implementation defined preprocessor extensions

  • #line - changes values of the __LINE__ and __FILE__ macros

A very common example of preprocessor usage is conditional compilation - providing different code depending on defined identifiers. Many are either implicitly defined by the compiler or passed as compiler options (-D for GCC and Clang). In the example below you can observe macro definition detection used to conditionally include platform-specific headers and platform-specific text replacement macros that represent a constant of maximum file name length.

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
#ifdef __linux__
	#include <unistd.h>
#elif defined(_WIN32)
	#include <windows.h>
#else
	#error unrecognized or unsupported platform
#endif

#include <string>

#ifdef _WIN32
std::string utf16_to_utf8(const wchar_t* wstr, int len)
{
	if (len == 0)
		return {};

	const int size = WideCharToMultiByte(CP_UTF8, 0, wstr, len, nullptr, 0, nullptr, nullptr);
	std::string result(size, '\0');
	WideCharToMultiByte(CP_UTF8, 0, wstr, len, &result.front(), size, nullptr, nullptr);
	return result;
}
#endif

std::string get_path_to_executable()
{
#ifdef _WIN32
	wchar_t buf[MAX_PATH];
	return utf16_to_utf8(buf, GetModuleFileNameW(nullptr, buf, MAX_PATH));
#else
	char result[PATH_MAX];
	const ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
	return std::string(result, (count > 0) ? count : 0);
#endif
}

This way the program can implement the same behavior on different platforms where different code is needed. Note that #ifdef does not behave as if - the preprocessor is run before compilation so it's not a runtime decision, it's a build-time decision which code to even start compiling.

After the preprocessing is done, compiler sees generated code without any preprocessor directives. Thus, the preprocessor can effectively be used to compile different code based on build settings and target platform information.

Applications

Apart from conditional compilation, complex replacement macros can be used to automate code generation. Abuse of this mechanism is generally considered bad style but still today some things have no better alternative - this is especially true for any kind of boilerplate that is not supported by templates or other language features. Typical applications of such macros are:

  • logging

  • assertions

  • unit tests

  • serialization