Show Lecture.ConditionalCompilation as a slide show.
CS253 Conditional Compilation
Buridan’s Ass
The Preprocessor
- There are several distinct phases to compilation:
- preprocessing: #include, #define, #undef, etc.
- code generation: C++ to machine language
- linking: combination of several *.o files & libraries
into a full executable.
- The preprocessor used to be a separate, standalone, program.
These days, it varies.
- The linker (ld, in Linux) is generally a separate program.
Preprocessor Directives: not just for #include guards!
#if condition | compile-time decision (compile the code or don’t) |
#ifdef symbol | was this compile-time symbol defined? |
#ifndef symbol | was this compile-time symbol not defined? |
#else | optional other half of #if or #ifdef |
#elif condition | another compile-time decision |
#endif | one for each #if or #ifdef |
#define | define a symbol (use constexpr instead) |
#error | preprocessor-time errors |
#pragma | implementation-defined |
Basic structure
cout << "alpha\n";
#if 2 > 3
cout << "beta\n";
#elif 4 != 5
cout << "gamma\n";
#endif
alpha
gamma
- We can make decisions at compile-time (as opposed to run-time)
via #if and friends.
- The code that failed the compile-time conditional test does
not get compiled.
- This is very different from a normal if, where the true & false
branches are both in the executable. For #if, the false branch is
eliminated at compile time, and does not appear in the executable
file.
- The false section doesn’t even have to be valid code, though it
must be valid tokens—it can’t contain
“Miles O'Brien”,
an unterminated character literal.
Comments
You can also “comment out” sections of code:
cout << "red\n";
#if 0
this does not work yet
cout << "white\n";
#endif
cout << "blue\n";
red
blue
Comments
- Of course, for that case, I could have commented out the lines
with conventional C++ comments.
- I could have used two
//
single-line comments; tedious for many lines.
- I could have used one big
/* … */
multi-line comment.
- Would
/* … */
work to comment out this allocate
function?
char *allocate(string /* type */) {
return new char[256]; // Just return maximum buffer for now
}
int main() {
cout << "Hello\n";
}
Hello
Conditional Debugging
You can use a flag to control conditional compilation:
const auto who = getenv("USER");
const auto pid = getpid();
#if DEBUG
cout << "who is " << who << '\n';
cout << "pid is " << pid << '\n';
#endif
string filename = "/tmp/temp-"s + who + '-' + to_string(pid);
cout << "filename: " << filename << '\n';
filename: /tmp/temp-cs253-870127
The flag DEBUG
isn’t defined, so the debug information isn’t printed.
Conditional Debugging
Let’s enable the flag using #define:
#define DEBUG 1
const auto who = getenv("USER");
const auto pid = getpid();
#if DEBUG
cout << "who is " << who << '\n';
cout << "pid is " << pid << '\n';
#endif
string filename = "/tmp/temp-"s + who + '-' + to_string(pid);
cout << "filename: " << filename << '\n';
who is cs253
pid is 870128
filename: /tmp/temp-cs253-870128
You can also turn the flag on via: g++ -D DEBUG=1
or g++ -D DEBUG
or g++ -DDEBUG
which are the same as:
#define DEBUG 1
Compiler Version
Most compilers provide ways to check the compiler version
at compile-time. The gcc/g++ compiler provides
several symbols:
cout << "Major " << __GNUC__ << '\n'
<< "Minor " << __GNUC_MINOR__ << '\n'
<< "Patch " << __GNUC_PATCHLEVEL__ << '\n'
<< "Version " << __VERSION__ << '\n';
Major 11
Minor 2
Patch 0
Version 11.2.0
cout << __GNUC__ << '.'
<< __GNUC_MINOR__ << '.'
<< __GNUC_PATCHLEVEL__ << '\n';
11.2.0
Compiler Version
You can use the symbols to decide whether a non-standard feature
is available:
#if __GNUC__ >= 50
// Only available on g++ starting with version 50
string filename = make_temp_file();
#else
// Do it the hard way:
const auto who = getenv("USER");
const auto pid = getpid();
string filename = "/tmp/temp-"s + who + '-' + to_string(pid);
#endif
cout << "filename: " << filename << '\n';
filename: /tmp/temp-cs253-870131
Compiler Version
We can’t do the former test using if:
string filename;
if (__GNUC__ >= 50)
// Only available on g++ starting with version 50
filename = make_temp_file(); // 🦡
else {
// Do it the hard way:
const auto who = getenv("USER");
const auto pid = getpid();
filename = "/tmp/temp-"s + who + '-' + to_string(pid);
}
cout << "filename: " << filename << '\n';
c.cc:4: error: ‘make_temp_file’ was not declared in this scope
Both parts of the normal if get compiled, but
make_temp_file()
isn’t defined on this compiler. Boom!
C++ Standard Version
For standard features, use __cplusplus, the YYYYMM of the standard in
effect. This uses vector::shrink_to_fit() when using C++11 or later;
better than guessing based on the compiler version:
vector<long double> v;
for (int i=1; i<=10000; i++)
v.push_back(i*i);
#if __cplusplus >= 201103L
const auto old = v.capacity();
v.shrink_to_fit(); // added in C++11
cout << old << " ➔️ " << v.capacity();
#endif
16384 ➔️ 10000
Why the L
in 201103L
?
__cplusplus is defined to be a long literal; this evades fussy
compiler warnings about long >= int
comparison.
System Detection
Every compiler defines symbols that allow you to
detect the compiler or architecture:
_WIN32
__APPLE__
__linux__
__GNUC__
__hpux
See https://sourceforge.net/p/predef/wiki/OperatingSystems/
System-Dependent Code
#if __APPLE__
cout << "On an Apple product\n";
#elif __linux__
cout << "On Linux\n";
#elif __hpux
cout << "On HP-UX\n";
#else
#error What is this, a Turing Machine!?
#endif
On Linux
- This code behaves differently on different systems.
- Cute, but is it useful? Yes.
Example
- Imagine that you want to write a function that returns
the current program name, for error messages.
- “No problem”, you say, “I can get that from
argv[0]
.”
- Yes, but that requires the user to pass argv to this function.
- How inconvenient!
- What if we’re nine levels deep in function calls?
- What if we’re writing a library, where we have no access to argv?
There is no standard solution to this problem.
Partial Solution
Most compilers have system-dependent ways of solving this problem.
The real problem is knowing which one to use. The Linux solution:
cout << "I am " << program_invocation_name;
I am ./a.out
HP-UX solution:
extern const char **__argv_value;
cout << "I am " << __argv_value[0];
/tmp/ccHoCpEw.ltrans0.ltrans.o: In function `main':
<artificial>:(.text.startup+0x18): undefined reference to `__argv_value'
collect2: error: ld returned 1 exit status
Naturally, the HP-UX solution doesn’t work on our Linux webserver. ☹
$ uname -o
GNU/Linux
Real solution
Of course, the real solution uses conditional compilation!
#if __linux__
string pname() { return program_invocation_name; }
#elif __hpux
string pname() {
extern const char **__argv_value;
return __argv_value[0];
}
#else
#error Good luck with your stone knives and bearskins.
#endif
int main() {
cout << pname() << '\n';
}
./a.out
Hide the OS-specific code in the definition of pname()
.
The rest of the program is clean, beautiful, and DRY.
False Solution
The test must occur at compile time—can’t define a function twice!
if (__linux__) { // 🦡
string pname() { return program_invocation_name; }
}
else if (__hpux) {
extern const char **__argv_value;
string pname() { return __argv_value[0]; }
}
else {
cerr << "I can’t deal with this system!\n";
}
int main() {
cout << pname() << '\n';
}
c.cc:1: error: expected unqualified-id before ‘if’