C++/Tutorial: 6. Präprozessor und Modularisierung
Nun, da wir unser Program mittels Funktionen in kleinere Abschnitte einteilen können, wollen wir uns der nächsten Hürde bei der Programmierung größerer Programm wittmen:
Zur Zeit ist alles in einer Datei. Das ist nicht nur ein Übersichtsproblem, sondern auch ein Zeitproblem, denn wenn wir eine Sache ändern, muss der gesamte Code neu kompiliert werden.
Wir wollen den Code also auf mehrere Dateien aufteilen, sodass jede Quellcodedatei einzeln zu einer Objektdatei kompiliert wird und dann die Objektdateien und die Bibliotheken zum fertigem Produkt(ausführbare Datei, Bibliothek) zusammen gelinkt werden, das ganze nennt sich Modularisierung(genauer gesagt die Zerlegung des Programmes in kleinere Entitäten). Modularisierung geht Hand in Hand mit dem Präprozessor, weswegen wir ihn ebenfalls in dem Kapitel besprechen(nicht nur die Themen, die zur Modularisierung gehören).
Also zunächst einmal, was ist eigentlich der Präprozessor? Bevor eine Sourcecodedatei kompiliert wird, wird der Präprozessor drüber laufen, er nimmt zum Beispiel kleinere textuelle Änderungen vor, das Resultat wird dabei normalerweise direkt an den Compiler weitergeleitet, sodass wir es nicht zusehen bekommen, aber meist kann man sich das Resultat dennoch ausgeben lassen, das ist dann allerdings Compilerspezifisch.
In der Tat haben wir bereits eine Präprozessordirektive verwendet, man erkennt Präprozessordirektiven an der etwas anderen Syntax: sie beginnen mit einem # und enden mit dem Zeilenende, die Rede ist natürlich von #include, auf das wir später noch einmal zurückkommen werden.
Inhaltsverzeichnis
Bedingte Kompilierung
Der erste Aufgabenbereich des Präprozessors, den wir besprechen wollen ist die sogenannte "Bedingte Kompilierung"
Genau genommen handelt es sich dabei einfach um Direktiven, die bestimmte Codeabschnitte auslassen, also dafür sorgen, dass sie einfach nicht kompiliert werden.
Der Sinn dahinter mag erst fragwürdig erscheinen, insgesamt ist es aber doch sehr sinnig um Sourcecode ohne Änderungen mit bestimmten Konfigurationen kompilieren zu können, oder auch um unter verschiedenen Plattformen verschiedenen Code zu kompilieren.
Für die bedingte Kompilierung sind verschiedene Präprozessordirektiven notwendig
- Makros, welche mit define definiert werden, meist kommt es nur darauf an, ob sie definiert sind, aber es ist auch möglich sie auf einen bestimmten Wert zu prüfen
- if, #elif, #else und #endif sowie der Präprozessoroperator defined oder die Kurzformen #ifdef und #ifndef um schließlich die Bedingung zu formulieren.
Die einfachste Form ein Makro mit #define zu definieren ist #define BEZEICHNER, üblicherweise werden die Bezeichner dabei vollständig in Großbuchstaben geschrieben, #if ist dann eine Direktive, die quasi einen Wahrheitswert (0 oder ungleich 0) erwartet (unnötig zu erwähnen, dass hier kein normaler Sourcecode verwendet werden kann, da der Präprozessor ja vor dem Compiler durchläuft) und bei 0 den Code bis zum nächsten #else, #elif oder #endif nicht kompiliert. #else ist dabei die Analogie zum C++-else und #elif die Analogie zu else if. Da #if im Gegensatz zum C++-if sich nicht auf eine Anweisung beschränkt, müssen wir den Wirkungsbereich irgendwie anders eingrenzen und das geschieht mit #endif. Nun fehlt uns nur noch der defined Präprozessoroperator, der einen Wahrheitswert zurückggibt, je nachdem, ob das angegebene Makro definiert wurde, oder nicht, auch können wir das Resultat mit dem ! Operator negieren, auch die anderen logischen Operatoren können wir anwenden. Ein einfaches Beispiel wäre
#include <iostream> #define DEBUG using namespace std; int main() { #if defined(DEBUG) cout << "execute main" << endl; #endif }
wenn wir das #define DEBUG nun entfernen(solche Makros kann man oft auch als Compilerschalter definieren, aber das ist Compilerspezifisch) wird auch execute main nicht mehr ausgegeben.
- if defined(MAKRO) und #if !defined(MAKRO) sind die beiden häufigsten Anwendungsfälle für #if, sodass es auch eine kürzere Schreibweise für diese gibt: #ifdef und #ifndef.
Außerdem gibt es noch die Möglichkeit Makros nicht länger zu definieren, #undef `löscht` ein Makro ab dem Punkt.
Ein Beispiel für Compilerdefinierte Makros: sleep
Auch der Compiler definiert Makros, die zu verwenden oft sehr sinnvoll sein kann. Die meisten davon sind nicht standardisiert, ein Beispiel für ein standardisiertes, also eines, das von jedem Compiler unterstützt wird wäre __cplusplus, welches von jedem C++-Compiler definiert wird, sinnvoll ist dies, wenn man weitestgehend C-kompatiblen Code schreibt und einige Dinge vom C-Compiler anders gemacht werden sollen, als vom C++-Compiler.
Eine häufige Anwendung ist jedoch auch die Programmierung für mehrere Plattformen, nehmen wir zum Beispiel eine sleep Funktion, die einfach nur 2 Sekunden warten soll. Diese Funktion ist in reinem C++ nicht vorhanden, aber die meisten Betriebssysteme unterstützen sie, sodass wohl die beste Möglichkeit wäre eine Bibliothek zu benutzen, die uns diese Funktion auf allen von uns gewünschten Plattformen zur Verfügung stellt. Nun gehen wir aber davon aus, dass wir diejenigen sind, die eine solche Bibliothek erstellen wollen, genauergesagt eine sleep-Funktion schreiben wollen, die auf mehreren Plattformen funktionieren soll.
Nehmen wir mal an unsere Sleepfunktion soll auf den Plattformen Windows, Linux und Mac OS X laufen. Dazu sollen die beiden populärsten Compiler GCC und Visual C++ unterstützt werden(letzteren natürlich nur unter Windows, für die anderen Plattformen gibt es ihn ja nicht).
Dazu benötigen wir folgende Informationen:
- Windows stellt eine Sleepfunktion "Sleep", die Millisekunden als Parameter nimmt, in <windows.h> zur Verfügung
- Unixartige Systeme, wie Linux oder Mac OS X stellen eine Sleepfunktion "sleep", die Sekunden nimmt, in <unistd.h> zur Verfügung.
- Sowohl MSVC, als auch der Windows-GCC-Port MinGW definieren das Makro _WIN32
- Der GCC definiert unter Unixartigen System das Makro __unix__
Diese Informationen reichen uns schon um eine Sleepfunktion zu definieren, die wir unabhängig von Plattform und Compiler verwenden können
#include <iostream> #if defined(__unix__) #include <unistd.h> #elif defined(_WIN32) #include <windows.h> #endif using namespace std; void win_and_unix_sleep(int seconds) { #if defined(__unix__) sleep(seconds); #elif defined(_WIN32) Sleep(seconds * 1000); #endif } int main() { cout << "Warte 2 Sekunden..." << endl; win_and_unix_sleep(2); cout << "... fertig" << endl; }
Präprozessorkonstanten
Eine weitere Anwendung von #define ist die Textersetzung mit Hilfe von symbolischen Konstanten. Symbolische Konstanten wurden vorwiegen noch in C-Zeiten eingesetzt und sollten in C++ vermieden werden, da sie keinen Typ haben, aber diese Typlosigkeit sorgt auch dafür, dass das Konstrukt etwas mächtiger ist als normale Konstanten.
Das Prinzip ist einfach, wir definieren eine Präprozessorkonstante mit #define BEZEICHNER WERT. Dabei wird anschließend jedes BEZEICHNER im Quellcode durch WERT ersetzt.
Wir könnten theoretisch schreiben #define grassland_path "12.png", aber das kann eine Reihe von Problemen hervorrufen: Nehmen wir zum Beispiel einmal an, wir schreiben gerade in einer Funktion und denken überhaupt nicht mehr an das grassland_path. Nun definieren wir dort eine Variable, die gleich heißt, also z.B. string grassland_path("13.png"), desto allgemeiner der Name gehalten ist desto wahrscheinlicher ist es, dass uns das aus Versehen einmal passiert. Hätten wir eine normale Konstante statt einem define an erster Stelle genutzt wäre das kein Problem, der innere scope verdeckt den äußeren und wir können in der Funktion problemlos mit dem inneren grassland_path arbeiten. Da wir aber ein #define genutzt haben und keine normale Konstante gibt es kein Scoping. Der Präprozessor ist dumm und ersetzt jedes grassland_path (Berichtigung zu einer vorherigen Version: der Präprozessor ist natürlich durchaus in der Lage Bezeichner zu erkennen und ersetzt grassland_path als Bestandteil von anderen Bezeichnern, innerhalb strings o.ä. nicht), sodass der Compiler dann folgendes zu gesicht bekommt: string "12.png"("13.png"). Das ist natürlich ein Fehler und wenn wir uns die Compilernachricht(z.B. beim GCC: error: expected unqualified-id before string constant) anschauen kommen wir schon zu einem zweiten Problem: der Compiler kennt den Bezeichner grassland_path gar nicht und deswegen werden die Fehlermeldungen häufig kryptisch.
Eine Sache ist noch anzumerken: was wenn wir eine Präprozessorkonstante mit mehreren Zeilen ersetzen wollen? Um anzugeben, dass eine Präprozessordirektive nicht mit dem Zeilenende beendet ist, muss an diesem ein \ stehen.
#include <iostream> #define PI 3.14 //zu vermeiden using namespace std; int main() { cout << PI << endl; }
Makros mit Parameter
Aber #define kann noch mehr, kommen wir zu der gefährlichsten, aber zugleich auch mächtigsten Form, Makros, die wie Funktionen aussehen.
Diese Makros ähneln den Präprozessorkonstanten sehr, nehmen aber noch einen Parameter, sodass sie wie Funktionen verwendet werden können. Früher hat man Makros verwendet um Performance zu gewinnen, da Makros auch nur eine Textersetzung sind und daher kein Funktionsaufruf benötigt wurde, heutzutage in C++ kann man aber auch Funktionen inlinen, was aber auch nicht notwendig ist, da vernünftige Compiler das normalerweise automatisch machen, wenn es sinnvoll ist.
Ein Makro definieren wir wie eine Symbolische Konstante nur mit einer Reihe von Parametern, die in Klammern folgt. Diese Parameter sind wie define generell nicht an irgendwelche Typen gebunden, sondern kann alles mögliche sein. Der Parameter kann dann in dem Text durch den das Makro ersetzt werden soll verwendet werden.
Ein Beispiel wäre
#define MAX(A, B) (A > B ? A : B)Sieht wie eine Funktion aus, ist aber keine, MAX(3,2) wird vom Präprozessor immer nur textuell durch (3 > 2 ? 3 : 2) ersetzt.
Das ganze verführt dazu einfache Funktionen als Makros zu definieren, aber das ist zu vermeiden, denn selbst einfache Funktionen als Makro zu definieren, sodass sich das Makro wie eine Funktion verhält ist alles andere als trivial. Nehmen wir zum Beispiel eine einfache square Funktion, die einen Wert quadrieren soll und ein SQUARE Makro, das das selbe tun soll.
#define SQUARE(X) X*X double square(double square) { return square*square; }
Auf den ersten Blick wirkt dies problemlos, das Makro kann sogar Typen, die sich nicht zu double konvertieren lassen quadrieren, sofern * auf sie angewand werden kann.
SQUARE(2) "gibt" 4 "zurück" und square(2) tut dies ebenfalls.
Nun square(2+3), das gibt wie erwartet 25 zurück, SQUARE(2+3) gibt 11 zurück, wieso? Die Parameter werden rein textuell eingesetzt, also wird SQUARE(2+3) einfach durch 2+3*2+3 ersetzt, was nicht das ist was wir wollten, also müssen wir unser Makro schon einmal abändern.
#define SQUARE(X) (X)*(X)und immernoch ist dieses Makro nichtmal annähernd perfekt. nehmen wir an, wir hätten eine Variable i, die den Wert 3 hat. square(i++) würde korrekterweise 9 zurückgeben, wobei i anschließend den Wert 4 hätte, SQUARE(i++) hingegen würde nicht nur 12 zurückgeben, sondern i hätte anschließend auch den Wert 5, denn SQUARE(i++) wird durch (i++)*(i++) ersetzt, i wird also zweimal inkrementiert und damit hätten wir schon ein schwer lösbares Problem. Wir dürften die Parameter des Makros nur einmal verwenden, aber ihn einfach einer Variable zuzuweisen geht auch nicht, weil wir das Makro dann nicht wie eine Funktion in einem Ausdruck verwenden könnte.
Wenn triviale Makros schon so problematisch sind, könnt ihr euch vorstellen, wie es mit komplexen Makros aussieht. Makros sind mächtig, es gibt einige gute Anwendungsgebiete für sie, aber sie sind zu vermeiden, wann immer möglich, denn sie führen leicht zu Fehlern und diese sind nichtmal leicht zu finden, da der Compiler, der den Fehler erst bemerkt, das Makro nicht kennt.
Code auf mehrere Dateien aufteilen
Kommen wir nun zur Modularisierung. Es ist in C++ ohne weiteres Möglich den Code auf mehrere Dateien aufzuteilen, also zum Beispiel eine Funktion in einer Datei zu definieren und in einer anderen Datei aufzurufen.
Wie wir bereits wissen, weiß der Compiler jedoch nichts von verschiedenen Dateien, diese werden lediglich vom Linker zusammen gelinkt und wir wissen auch, dass wir in C++ nur deklarierte Funktionen aufrufen können. Es ist also notwendig, dass Funktion in jeder Datei, in der sie verwendet wird deklariert wird.
//In Datei main.cpp void greet(); int main() { greet(); } //In datei greet.cpp: #include <iostream> using namespace std; void greet() { cout << "Guten Tag" << endl; }
Funktionen können wir also bereits problemlos in mehreren Dateien verwenden.
Wie sieht es mit globalen Variablen aus? Schließlich sind diese doch global, also müssten wir sie auch in anderen Dateien verwenden können, wenn wir die Variable aber in beiden Dateien deklarieren, kriegen wir einen Fehler. Dieser Fehler ist ähnlich dem, den wir kriegen würden, wenn wir eine Funktion doppelt definieren(nicht nur deklarieren). Das ist logisch, denn Variablen haben keine Trennung zwischen Deklaration und Definition, wie bei Funktionen, wir definieren die Variable doppelt, aber sie darf nur einmal existieren.
Wir brauchen also einen Weg ähnlich wie bei Funktionen dem Compiler mitzuteilen, dass irgendwo eine globale Variable mit dem Namen existiert, ohne jedoch eine neue Variable zu deklarieren. Dazu gibt es das Schlüsselwort extern, welches der Variablendeklaration vorangestellt wird um zu sagen, dass die Variable nicht neu angelegt werden soll, sondern bereits in einer anderen Datei existiert.
Folgendes ist schlechter Stil, wie globale Variablen allgemein, demonstriert aber die Funktionsweise von extern
//In Datei main.cpp #include <string> using namespace std; string greeting_text; void greet(); int main() { greeting_text = "Hiho"; greet(); } //In Datei greet.cpp #include <iostream> using namespace std; extern string greeting_text; void greet() { cout << greeting_text << endl; }
Dateilokale globale Variablen und Funktionen
Nehmen wir aber an, wir brauchen eine Variable nur in einer Datei und wir wollen nicht, dass irgendjemand außerhalb der Datei auf diese Variable zugreifen kann, oder aber wir wollen es möglich machen, zwei Variablen mit dem selben Namen in verschiedenen Dateien zu definiern, dann brauchen wir ein Mittel um die Variable auf eine Datei zu beschränken, dafür gibt es 2 verschiedene Sprachmittel.
static
Aus C übernommen wurde die Anwendung des Schlüsselwortes static auf globale Variablen. static hat hier nichts mit dem static in Funktionen (oder später dem in Klassen) zu tun, sondern heißt nur gleich. Die Intention von static in C war eher die letztere, Variablen mit dem selben Namen in verschiedenen Dateien zu ermöglichen.
static wird der betreffenden Variable oder Funktion vorangestellt um zu indizieren, dass die Variable oder Funktion nur für diese Datei definiert ist.
//main.cpp #include <string> using namespace std; static string greeting_text; void greet(); int main() { greeting_text = "Hiho"; greet(); } //greet.cpp #include <iostream> using namespace std; static string greeting_text; void greet() { cout << greeting_text << endl; }
würde also dafür sorgen, dass wir 2 verschiedene Variablen "greeting_text" haben, und da wir dem greeting_text in greet.cpp nie einen Wert zugewiesen haben, wird auch nichts ausgegeben
Anonyme Namespaces
C++ hat dann ein Sprachmittel eingeführt, was static im Prinzip überflüssig macht, denn es erfüllt den selben Zweck: den anonymen Namespace. Sämtlicher Code, der in einem namespace { } eingeschlossen ist, existiert nur für diese Übersetzungseinheit. Unser obiges Beispiel würde mit anonymen Namespaces folgendermaßen aussehen und identisch agieren
//main.cpp #include <string> using namespace std; namespace { string greeting_text; } void greet(); int main() { greeting_text = "Hiho"; greet(); } //greet.cpp #include <iostream> using namespace std; namespace { string greeting_text; } void greet() { cout << greeting_text << endl; }
Wir könnten an dieser Stelle auch greet in den anonymen Namespace verschieben, dann würde der Code nicht mehr funktionieren, da main.cpp nicht auf das greet() aus greet.cpp zugreifen könnte.
Headerdateien und #include
Mit den bisherigen Mitteln können wir unseren Code zwar bereits auf mehrere Dateien aufteilen, aber ist es unpraktikabel immer alle Deklarationen erneut aufzuschreiben.
Man kommt zwar nicht drum herum, alles immer für jede Übersetzungseinheit erneut zu deklarieren, aber als Lösung für dieses Problem verwendet man sogenannte "Headerdateien", das sind Dateien(üblicherweise mit der Endung *.h oder *.hpp), in denen alle Deklarationen stehen und die vom Präprozessor in jede .cpp Datei eingefügt werden(wenn so gewünscht).
Um das zu verdeutlichen würden wir das am Beispiel der greet() Funktion in greet.cpp folgendermaßen machen: wir würden eine Datei greet.hpp erstellen und in sie zunächst lediglich
void greet();
schreiben, dann würden wir den Präprozessor anweisen den Inhalt dieser Datei in die main.cpp Datei einzufügen. Um dies zu bewerkstelligen benötigen wir die #include Direktive, gefolgt vom Dateinamen eingeschlossen in Anführungszeichen. Wir hätten also 3 Dateien:
//main.cpp #include <string> #include "greet.hpp" using namespace std; int main() { greet(); } //greet.cpp #include <iostream> #include "greet.hpp" using namespace std; void greet() { cout << "Guten Tag" << endl; } //greet.hpp void greet();
In greet.hpp stehen die Deklarationen für greet.cpp. Es ist keinesfalls notwendig greet.hpp in greet.cpp zu inkludieren, dennoch ist es keine schlechte Idee, weil es in vielen Fällen eben doch notwendig ist, für uns bedeuted es zunächst, dass wir nicht auf die Reihenfolge der Funktionsdefinitionen achten müssen.
Es ist auch nicht notwendig, dass greet.hpp den selben Namen hat wie greet.cpp, aber da es üblich ist für jede Sourcedatei eine Headerdatei zu haben(außer vielleicht für die Datei mit der main-Methode) ist auch das keine schlechte Konvention.
#include mit " " und #include mit <>
Wir kennen die #include Direktive bereits, wir haben sie immer verwendet, wenn wir Dinge aus der Standardbibliothek verwenden wollten. Jetzt wissen wir auch warum: Wir haben diese Typen und Funktionen damit für uns erst deklariert. Alles logisch, aber einen Unterschied gibt es noch, die Dateien aus der Standardbibliothek werden mit <> inkludiert, während wir #include mit "" verwenden, wo liegt der Unterschied? Der Unterschied liegt lediglich darin, wo die Dateien (zu erst) gesucht werden, <> sucht zuerst in den Compilerverzeichnissen, wird also dann verwendet, wenn wir auf die Standardbibliothek oder irgendeine andere Bilbiothek zugreifen wollen(normalerweise sagen wir dem Compiler wo die Bibliotheksdateien zu finden sind), "" verwenden wir dann, wenn es sich um unsere eigenen Dateien handelt, das ist der einzige Unterschied.
Anmerkung: Vielleicht ist noch jemandem aufgefallen, dass die Header der Standardbiliothek nicht auf .h oder .hpp enden. Früher war das mal der Fall, aber mit dem C++ Standard aus dem Jahre 1998 wurden viele Dinge, wie namespaces eingefügt, die die Header grundlegend veränderten. Um keine Inkompatibelitäten hervorzurufen, wurden die Standardheader dann umbenannt, sodass sie nicht mehr auf .h enden und die C-Kompatibelitätsheader zusätzlich noch ein c als Prefix haben(math.h -> cmath), während diese Header mit .h von einigen Compilern noch unterstützt werden um abwärtskompatibel zu bleiben. Das ist übrigens ein gutes Indiz für ein schlechtes Buch oder Tutorial, wenn die Standardheader dort noch mit .h enden (#include <iostream.h> z.B.) ist das Buch oder Tutorial hoffnungslos veraltet und unmodern, sodass es weggeschmissen gehört.
Include Guards
Nehmen wir nun einmal an, wir haben eine Funktion in einer Datei a.hpp deklariert. Nun brauchen wir diese in der Datei b.hpp (bei Funktionen kommt man zwar drumherum, indem man in Headerdateien grundsätzlich nur Deklarationen hat, aber bei Typen wie std::string kommt man nicht mehr drumherum), also inkludieren wir a.hpp in b.hpp. Nun nehmen wir noch an, wir hätten eine unabhängige Datei c.hpp, diese braucht eine Funktion aus a.hpp und b.hpp. Da b.hpp a.hpp schon inkludiert, würde es reichen nur b.hpp zu inkludieren und alles würde funktionieren, aber damit sind drei Probleme verbunden:
- Wir wissen nicht unbedingt, welcher Header welchen anderen Header inkludiert, immer nachzuschauen wäre ein großer Aufwand.
- Wenn der Entwickler von b.hpp nun entscheidet a.hpp nicht mehr zu inkludieren müssen wir das wieder selbst tun, unser Code würde zuerst einmal brechen.
- Es kann von der Implementierung abhängen, limits muss mit dem MSVC für die Verwendung mit cin nicht mehr inkludiert werden, mit dem GCC allerdings schon
Also wäre es besser a.hpp selbst zu inkludieren, aber wenn wir das tun, haben wir a.hpp doppelt inkludiert und damit die Funktionen doppelt deklariert, was ein Fehler ist.
Wir brauchen also eine Möglichkeit Header nur zu inkludieren, wenn sie noch nicht bereits inkludiert wurden und diese haben wir bereits. Man braucht dafür in C++ nämlich kein neues Sprachmittel, sondern lediglich die bedingte Kompilierung, das ganze ist aber so üblich, dass die Technik einen eigenen Namen hat: Include Guards.
Das ganze würde für unser Beispiel mit greet.hpp so aussehen:
#ifndef GREET_HPP_INCLUDED #define GREET_HPP_INCLUDED void greet(); #endif
Diese Includeguards sollten immer vorhanden sein, wenn man keinen Grund hat sie wegzulassen(um Zeit zu sparen sind sie bei vielen IDEs standardmäßig in der Datei zu finden, wenn man angibt eine Headerdatei erstellen zu wollen).
#pragma
Die letzte Präprozessordirektive, die ich hier besprechen möchte(andere in späteren Artikeln) ist #pragma. Wenn man sich rein nach dem C++ Standard richtet darf man es gar nicht verwendet, denn #pragma ist dafür dar, dass Compilerherrsteller eigene Direktiven einführen können, wenn diese Direktive nicht existiert wird das #pragma ignoriert.
Eine weit verbreitete Direktive ist #pragma once. Wenn diese Direktive in einer Datei steht, kann man auf die Includeguards verzichten und bei manchen Compilern (dem GCC z.B. nicht) hat man dann noch einen Vorteil in der Kompilierungsgeschwindigkeit.
Solche pragmas sind gefährlich, weil sie den Code brechen, wenn sie nicht vorhanden sind. Es gibt auch völlig ungefährliche pragmas, wie region und endregion in Microsoft Visual Studio[1], diese bewirken leglich eine Möglichkeit der Codefaltung im Editor und haben keinen semantischen Einfluss auf das Programm. Es gibt auch pragmas, die einen Einfluss auf das resultierende Programm haben und ungefährlich sind, weil sie zum Beispiel nur aus Performancegründen existieren, wie zum Beispiel #pragma optimize in Visual Studio.