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 |
#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 syntactically valid.
Comments
You can also “comment out” sections of code:
cout << "red\n";
#if 0
this doesn’t 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-721633
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 721634
filename: /tmp/temp-cs253-721634
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 8
Minor 3
Patch 1
Version 8.3.1 20191121 (Red Hat 8.3.1-5)
cout << __GNUC__ << '.'
<< __GNUC_MINOR__ << '.'
<< __GNUC_PATCHLEVEL__ << '\n';
8.3.1
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-721637
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/cc2H0Vb6.o: In function `main':
c.cc:(.text+0x19): 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 {
// I can’t deal with this system!
}
int main() {
cout << pname() << '\n';
}
c.cc:1: error: expected unqualified-id before 'if'