Mehr Präprozessor

Der Präprozessor ist nicht nur dazu da, Dateien zusammenzukopieren und an den Compiler zu übergeben. Er stellt noch eine ganze Menge zusätzlicher Möglichkeiten bereit, Ersetzungen im Code vorzunehmen.

Video

Code

Als Demonstration für einige Fähigkeiten des Präprozessors dienen die beiden Dateien logging.hpp und test.cpp

logging.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#ifndef LOGGING_HPP
#define LOGGING_HPP

#ifndef LOGTARGET
#define LOGTARGET std::cout
#endif // LOGTARGET

#ifdef NDEBUG
    #define TRACE(msg, file, line)
#else
    #define TRACE(msg, file, line) LOGTARGET << "[TRACE] " << \
            msg << " (at " << file << ":" << line << ")\n"
#endif //NDEBUG

#define ENTER(fkt) TRACE("ENTER " fkt, __FILE__, __LINE__)
#define EXIT(fkt) TRACE("EXIT " fkt, __FILE__, __LINE__)

#define CALL(fktcall) TRACE("CALL " #fktcall, __FILE__, __LINE__); fktcall;

#endif // LOGGING_HPP

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include "logging.hpp"

void test() {
    ENTER("test");
    std::cout << "Testing\n";
    EXIT("test");
}

int main(int argc, char * argv[]) {
    ENTER("main(int,char*[])");
    std::cout << "Hello world!\n";
    CALL(test());
    EXIT("main(int,char*[])");
}

Erklärung

Die Möglichkeiten des Präprozessors, den Code zu modifizieren, bevor er zum Compiler geht, lassen sich grob in verschiedene Kategorien einteilen.

Bedingte Compilierung

Unter Umständen kann es notwendig sein, Code von der Übersetzung durch den Compiler auszuschließen. Das können bspw. plattformabhängige Stellen im Code sein, wenn Code auf mehrere Betriebssysteme portiert werden soll. Dazu stellt der Präprozessor die Anweisungen #if, #ifdef und #ifndef zur Verfügung. Diese können bestimmte Bedingungen prüfen und abhängig davon Code-Abschnitte ein- und ausblenden. Abschnitte beginnen immer nach der entsprechenden Präprozessor-Direktive und enden am nächsten #elif, #else oder #endif

Symbole definieren: #define/#undef

Die Befehle #define und #undef definieren Präprozessorsymbole oder löschen deren Definition. #define nimmt optional zusätzlich zum Symbolnamen einen Wert, den dieses Symbol annehmen soll. Wird das Symbol dann irgendwo im Quelltext verwendet, so wird dieser Wert (ggf. rekursiv, falls im Wert wiederum Präprozessorsymbole verwendet werden) aufgelöst.

Dateien einbinden

Externe Dateien können mit #include eingebunden werden. Dabei findet eine (ggf. rekursive) Ersetzung der #include-Anweisung durch den Dateiinhalt statt. Die beiden Formen mit <...> und "..." unterscheiden sich in der Quelle, aus der die Header eingebunden werden. <...> bindet Systemheader ein. Hierbei ist nicht genauer vorgegeben, wo und wie diese vorliegen. Meist finden sich diese in fest konfigurierten Pfaden in der Compiler-Installation. Quasi alle Kompiler erlauben die Erweiterung des Systemsuchpfades durch Kommandozeilenparameter (bspw. zur Einbindung zusätzlicher Bibliotheksheader). "..." bindet Dateien aus lokalen Pfaden ein. Der übergebene Pfad wird hierbei immer ausgehend von der gerade kompilierten Datei aufgelöst. Bei rekursiven Einbindungen wird ebenso ab dem Pfad der einbindenden Datei gesucht (bspw. relevant, wenn ein Bibliotheks-Header wiederum andere Header relativ einbindet).

Vordefinierte Symbole

Compiler können bestimmte Symbole vordefinieren, die vom Präprozessor genutzt werden können, um Code einzubinden etc.

Standardmäßig vordefinierte Symbole sind:

Quasi alle relevanten Compiler und Bibliotheken definieren eine Fülle zusätzlicher Symbole vom aktuell verwendeten C++-Standard bis zu existierenden Bibliotheken, um eine Anpassung des eigenen Codes an die Umgebung zu ermöglichen.