CMake-Compiler-Flags – Targets individuell kompilieren

()

Compiler bieten eine riesige Anzahl von Compiler-Flags, die verschiedenen Optionen beim Erstellen von Build-Dateien ermöglichen. Bei direkter Verwendung des Compilers übergibt man die Compiler-Flags direkt auf der Kommandozeile. Verwendet man jedoch CMake, gibt es dort verschiedenen Befehle, um Compiler-Flags zu übergeben. In diesem Beitrag stelle ich euch diese Befehle vor, gebe dazu mehrere Beispiele und gehe auch auf verschiedene Compiler ein. Zumeist übernimmt CMake auch die Aufgabe, zwischen verschiedenen Compilern zu unterscheiden.

Diesen und andere Beiträge von mir kannst du als sauber formatierte PDF-Datei zum Ausdrucken oder offline Lesen erwerben. Mehr Informationen dazu findest du hier.

Nötige Vorkenntnisse

  • CMake Grundkenntnisse: Dazu gehört etwa die Erstellung und Kompilierung eines ausführbaren Programms mit CMake. Ihr solltet dahin gehend auch mit den CMake-Befehlen cmake_minimum_required(), project() und add_executable() vertraut sein. Solltet ihr nicht wissen, wie ihr ein Programm mit CMake erstellt und kompiliert, schaut euch am besten zuvor diesen Artikel oder dieses YouTube-Video von mir an.
  • CMake-Variablen: CMake-Variablen stelle ich sowohl in einem Artikel als auch in einem YouTube-Video vor. Zudem findet sich eine Erklärung zu CMake-Variablen auch in der CMake-Dokumentation.
  • CMake-Properties: In diesem Video auf YouTube gehe ich auf Properties in CMake ein. Zudem findet man eine Auflistung von Properties in der CMake-Dokumentation, eine genauere Erläuterung, was Properties eigentlich sind, fehlt hier jedoch.
  • CMake-Listen: Listen sind in CMake ein einzelner zusammenhängender, langer String, in der die einzelnen Werte mittels eines Semikolons getrennt werden. Mehr Informationen zur Handhabung von Listen findet ihr in diesem YouTube-Video oder in der CMake-Dokumentation.
  • PUBLIC, PRIVATE, INTERFACE: Diese Keywords dienen dazu, den Scope der übergebenen Flags zu definieren. Sie kommen in vielen CMake-Befehlen, wie target_include_directories() oder target_link_libraries() zum Einsatz. In der CMake-Dokumentation zu diesen Befehlen oder in diesem YouTube-Video findet ihr dazu mehr Informationen.
  • C++-Sprachstandard: In Beispiel 2 setze ich den C++-Sprachstandard einmal über CMake-Variablen und einmal durch eine Compiler-Flag. In diesem Beitrag habe ich ausführlich über die Verwendung des C++-Standards in CMake geschrieben. Ebenfalls wird man dazu in der CMake-Dokumentation oder in meinem YouTube-Video über Properties fündig.
  • If-Verzweigungen: If-Verzweigungen in CMake unterscheiden sich generell nicht von if-Verzweigungen in anderen Programmiersprachen und sollten daher wohl den meisten Lesern geläufig sein. If-Verzweigungen in CMake erwähne ich kurz in diesem YouTube-Video, ansonsten hilft hier natürlich auch wieder die CMake-Dokumentation weiter.

Compilerspezifische Target-Properties

Jedes Target enthält eine Vielzahl von Properties, die die Übergabe von Flags an den Compiler des Targets beeinflussen. Die vier wichtigsten sind INCLUDE_DIRECTORIES, COMPILE_DEFINITIONS,
COMPILE_FEATURES und COMPILE_OPTIONS. In der Property INCLUDE_DIRECTORIES wird die Liste der Ordner gespeichert, die dem Compiler als Suchpfade für Dateien übergeben werden. Dies geschieht bei den meisten Compilern mittels -I Ordner respektive /I Ordner. Üblicherweise sollte dies über den CMake-Befehl target_include_directories() gehandhabt werden. Da diese Property und der target_include_directories()-Befehl thematisch eher in den Bereich Bibliotheken und Projektstrukturierung passen, gehe ich in diesem Beitrag nicht weiter darauf ein. Bleiben die drei letztgenannten Properties, auf die ich mich in diesem Beitrag konzentrieren werde.

COMPILE_DEFINITIONS

Diese Property ist eine CMake-Liste, in der Definitionen gespeichert sind, die auf der Kommandozeile übergeben werden sollen. Eine Definition hat entweder die Form DEF oder DEF=Wert. CMake konvertiert die angegebenen Definitionen dann in die entsprechende Compiler-Flag, die vorwiegend die Form -D DEF respektive -D DEF=WERT hat. Im C++-Code können diese Definitionen, oft auch als Symbole bezeichnet, beispielsweise mittels des Makros #ifdef überprüft werden.

COMPILE_FEATURES

CMake ermöglicht die Angabe einzelner sogenannter „Features“ von Sprachstandards (zum Beispiel C++11) für Targets. Ein solches Feature sind etwa die „constant expressions“ (constexpr) aus dem C++11-Standard. Auch die Angabe des Sprachstandards ist an dieser Stelle möglich. CMake übernimmt die Aufgabe, den korrekten Sprachstandard anhand der gegebenen Features zu ermitteln und dem Compiler zu übergeben. Bei Überschneidungen nimmt CMake immer den höheren C++-Standard. In der globalen CMake-Property CMAKE_CXX_KNOWN_FEATURES ist eine Liste aller verfügbaren C++-Features gespeichert. Zusätzlich findet man diese in der CMake-Dokumentation. Welche C++-Features der verwendete Compiler unterstützt, wird in der CMake-Variablen CMAKE_CXX_COMPILE_FEATURES gespeichert.

COMPILE_OPTIONS

In dieser Property werden alle übrigen Compiler-Flags in einer CMake-Liste gespeichert, die nicht in die beiden obigen Properties passen. Diese Liste wird dann exakt so an den Compiler übergeben. Im Gegensatz zu anderen Aufgaben, wie das Einbinden von Ordnern oder das Verlinken von Bibliotheken über die entsprechenden CMake-Befehle, wandelt CMake direkt übergebene Compiler-Flags über diese Property nicht automatisch in die korrekte Form für jeden Compiler um. Bei Verwendung von unterschiedlichen Compilern, insbesondere des MSVC-Compilers, muss also auf die unterschiedliche Form der Compiler-Flags geachtet werden.

Weitere Informationen

Die vier genannten Target-Properties haben jeweils auch ein Gegenstück, das den Präfix INTERFACE_ trägt, also zum Beispiel INTERFACE_COMPILE_DEFINITIONS. Diese haben den gleichen Effekt wie ihre Namensvetter, beziehen sich aber nur auf verlinkte Targets. Damit können Anforderungen an verlinkte Targets vorgegeben werden, die von diesem Target benötigt werden.

Dadurch, dass jedes Target seine eigenen Properties besitzt, kann jedes Target kann mit einem unterschiedlichen Satz von Compiler-Flags kompiliert werden, spezifiziert durch die zugehörigen Properties. So können etwa Targets mit unterschiedlichen Definitionen und unterschiedlichen C++-Sprachstandards kompiliert werden.

Übergabe von Flags an den Compiler

Wenden wir uns nun der Möglichkeit zu, Flags an den Compiler für ein bestimmtes Target zu übergeben. Für die im Folgenden targetspezifischen Befehle gibt es jeweils auch einen global wirkenden Befehl, der sich auf alle Targets auswirkt. Wie in CMake ab Version 3.0 üblich sollten jedoch die targetspezifischen Befehle verwendet werden. Daher gehe ich an dieser Stelle auch nicht weiter auf die globalen Befehle ein.

Übergabe von Definitionen -D

Blicken wir jetzt einmal auf den CMake-Befehl target_compile_definitions(), mit dessen Hilfe Definitionen mittels des Compilers übergeben werden können. Diese Definitionen können dann im C++-Code überprüft oder verwendet werden.

target_compile_definitions(
  <TargetName>
  <INTERFACE|PUBLIC|PRIVATE> [<Definition1> ...]
  [<INTERFACE|PUBLIC|PRIVATE> [<Definition2> ...] ...]
)

Nach der Angabe des Targets <TargetName> an den die Definition übergeben werden soll, folgt eines der drei Keywords INTERFACE, PUBLIC oder PRIVATE. Bei Angabe des Keywords PRIVATE wird die übergebene Definition <Definition1> in die Property COMPILE_DEFINITIONS geschrieben, womit diese Definition nur für das Target <TargetName> gültig ist. Bei Verwendung des Keywords INTERFACE wird die Definition <Definition1> in der Interface Property INTERFACE_COMPILE_DEFINITIONS gespeichert. Somit ist diese Definition nur für verlinkte Targets relevant. Bei Verwendung des Keywords PUBLIC wird die Definition <Definition1> in beide Properties geschrieben.

Die Definitionen <Definition1>, <Definition2> usw. müssen die Form DEFINITION oder DEFINITION=Wert haben. Es sind zwar auch Kleinbuchstaben möglich, doch die Verwendung dieser ist sehr ungewöhnlich. Wie im obigen Befehl zu sehen, ist es möglich mehrere Definitionen auf einmal mit einem der Keywords INTERFACE, PUBLIC oder PRIVATE zu setzen und/oder mehrere Keywords mit verschiedenen Definitionen zu verwenden.

Beispiel 1: Übergabe und Verwendung von Definitionen

In diesem Beispiel übergebe ich zwei Definitionen mittels des Compilers an eine ausführbare Datei. Innerhalb der ausführbaren Datei greife ich auf die übergebenen Definitionen zu. Schauen wir dazu zunächst die CMakeLists.txt-Datei an:

cmake_minimum_required(VERSION 3.8...3.26)

project(compiler_flag_1 LANGUAGES CXX)

add_executable(def_test main.cpp)

target_compile_definitions(
  def_test
  PRIVATE PRINT NUMBER=1
)

In den ersten Zeilen passiert erst einmal nichts Spezielles, wir erstellen lediglich die ausführbare Datei def_test aus der Source-Datei main.cpp in Zeile 5. Dieser ausführbaren Datei fügen wir nun die beiden Definitionen PRINT und NUMBER=1 unter dem Keyword PRIVATE hinzu. Da das Target def_test nicht weiter verlinkt wird und keine anderen Targets involviert sind, ist das Keyword PRIVATE an dieser Stelle die richtige Wahl. Diese beiden Definitionen verwenden wir nun in der Source-Datei main.cpp:

#include <iostream>

int main() {

#ifdef PRINT
  std::cout << "PRINT is defined." << std::endl;
#else
  std::cout << "PRINT not defined." << std::endl;
#endif

  std::cout << "NUMBER: " << NUMBER << std::endl;

  return 0;
}

In den Zeilen 5 bis 9 wird mittels der Makros #ifdef, #else und #endif überprüft, ob das Symbol PRINT an dieser Stelle definiert ist oder nicht. Je nachdem wird an dann von dem Programm „PRINT is defined.“ oder „PRINT not defined.“ ausgegeben. In Zeile 11 wird dann die Zahl ausgegeben, die in der Definition NUMBER gespeichert ist. Sollte NUMBER an dieser Stelle nicht definiert sein, wird ein Fehler vom Compiler ausgegeben. Ich spare mit an dieser Stelle die Ausgabe der Kompilierung des Programms, da für dieses Beispiel keine relevante zusätzliche Ausgabe erfolgt. Die Ausgabe des Programms def_test ist dann wie folgt:

$ ./def_test 
PRINT is defined.
NUMBER: 1

Übergabe von Compiler-Features

Der CMake-Befehl target_compile_features() ist genauso aufgebaut wie der target_compile_definitions()-Befehl. Mithilfe dieses CMake-Befehls können der benötigte Sprachstandard oder aber auch einzelne Features eines Standards aktiviert werden. Diese „Aktivierung“ erfolgt durch die Übergabe der korrekten Flags an den Compiler, wobei CMake selbst die benötigten Flags und die Art wie diese dem Compiler übergeben werden ermittelt.

target_compile_features(
  <TargetName>
  <INTERFACE|PUBLIC|PRIVATE> [<Feature1> ...]
  [<INTERFACE|PUBLIC|PRIVATE> [<Feature2> ...] ...]
)

Wie auch beim target_compile_definitions()-Befehl, bestimmt die Verwendung des Keywords INTERFACE, PUBLIC oder PRIVATE, ob das Feature <Feature1>, <Feature2> usw. in die Property COMPILE_FEATURES oder INTERFACE_COMPILE_FEATURES geschrieben wird. Die Property COMPILE_FEATURES ist für das angegebene Target <TargetName> relevant, während die Property INTERFACE_COMPILE_FEATURES für verlinkte Targets relevant ist.

In der CMake-Variablen CMAKE_CXX_KNOWN_FEATURES ist eine Liste aller verfügbaren C++-Features gespeichert. Zusätzlich findet man diese in der CMake-Dokumentation. Welche C++-Features der verwendete Compiler unterstützt, wird in der CMake-Variablen CMAKE_CXX_COMPILE_FEATURES gespeichert. Ab C++17 existieren keine individuellen Features mehr, stattdessen sollen die High-Level-Features für den Sprachstandard wie cxx_std_17, cxx_std_20 usw. verwendet werden. Dies gilt allgemein auch für frühere C++-Standards, auch wenn für diese spezifische Sprachfeatures zur Verfügung stehen. In meinem Beitrag zum C++-Standard habe ich noch ausführlicher über die Verwendung von Compiler-Features geschrieben, insbesondere über die individuellen Features.

Beispiel 2: Verwendung von C++-17 durch Compiler-Features

In diesem Beispiel setze ich den C++-Sprachstandard einmal über CMake-Variablen und einmal über den oben gezeigten Befehl target_compile_features(). Schauen wir dazu zunächst auf die CMakeLists.txt-Datei.

cmake_minimum_required(VERSION 3.8...3.26)

project(compiler_flag_2 LANGUAGES CXX)
    
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(cpp17_exe main.cpp)

target_compile_features(
  cpp17_exe 
  PRIVATE cxx_std_17
)

In den Zeilen 5–7 wird mittels der bekannten CMake-Variablen der C++11-Standard für alle Targets gesetzt. Die erzeugte ausführbare Datei cpp17_exe (ein Target) in Zeile 9 benötigt jedoch den C++17-Standard, wie wir gleich in der zugehörigen main.cpp-Datei sehen werden. Um nur speziell dieses Target mit dem C++17-Standard zu kompilieren, verwenden wir in Zeile 11–14 den target_compile_features()-Befehl. In diesem geben wir den C++17-Standard mittels des Compiler-Feature cxx_std_17 an. Blicken wir nun in die main.cpp-Datei:

#include <iostream>

int main() {
  int position[2] = {1, 2};

  auto [x, y] = position;

  std::cout << x << " " << y << std::endl;

  return 0;
}

In Zeile 7 werden sogenannte „structured bindings“ aus dem C++17-Standard verwendet. Dabei werden den Variablen x und y die Werte aus dem Array position zugewiesen. Anschließend werden die Variablen x und y in Zeile 8 ausgegeben. Die Ausgabe des Programm cpp17_exe sieht dann wie folgt aus, wobei ich erneut auf die Ausgabe der Kompilierung verzichte:

$ ./cpp17_exe 
1 2

Übergabe von Compiler-Optionen

Der Befehl target_compile_options() ermöglicht die Übergabe von Compiler-Flags, die nicht über die beiden erstgenannten Befehle in diesem Beitrag abgedeckt werden. Zudem muss man bei diesem Befehl darauf achten, dass die übergebenen Compiler-Flags nicht automatisch an den verwendeten Compiler angepasst werden.

target_compile_options(
  <TargetName>
  [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [<Option1> ...]
  [<INTERFACE|PUBLIC|PRIVATE> [<Option2> ...] ...]
)

Der Befehl target_compile_options() bietet, im Gegensatz zu den anderen beiden Befehlen, noch das optionale Keyword BEFORE. Bei Verwendung dieses Keywords wird die übergebene Option <Option1>, <Option2> usw. nicht an das Ende der Property COMPILE_OPTIONS bzw. INTERFACE_COMPILE_OPTIONS gehangen und damit an das Ende des Compiler-Aufrufs, sondern direkt an den Anfang. Wie bei den anderen beiden Befehlen auch, entscheidet die Verwendung der Keywords INTERFACE, PUBLIC oder PRIVATE darüber, in welche Properties die übergebenen Compiler-Optionen geschrieben werden.

Beispiel 3: Aktivierung von Compiler-Warnungen

In diesem Beispiel setze ich eine Compiler-Flag, um die zusätzliche Warnungen des Compilers zu aktiveren. Ich verwende zusätzlich eine if-Verzweigung, um unterschiedliche Compiler abzudecken. Schauen wir dazu zunächst auf die CMakeLists.txt-Datei:

cmake_minimum_required(VERSION 3.8...3.26)

project(compiler_flag_3 LANGUAGES CXX)
    
add_executable(warning_test main.cpp)

if(MSVC) 
  target_compile_options(
     warning_test
     PRIVATE "/W4"
   )
else()
  target_compile_options(
     warning_test
     PRIVATE "-Wall"
   )
endif()

In Zeile 5 wird die ausführbare Datei warning_test aus der Source-Datei main.cpp erstellt. Für dieses Target wollen wir nun Compiler-Warnungen aktivieren. Bei den meisten Compilern kann dazu die Compiler-Flag -Wall verwendet werden, die eine große Anzahl an Warnungen aktiviert. Die Verwendung dieser Compiler-Flag ist zwar auch mit dem MSVC-Compiler möglich, erzeugt jedoch eine große Menge an Warnungen aus inkludierten Header-Dateien, die man nicht kontrollieren kann. Zudem ist die Syntax für Compiler-Flags im MSVC-Compiler etwas anders. Daher wird für den MSVC-Compiler die Compiler-Flag /W4 verwendet. Eine entsprechende if-Verzweigung in den Zeilen 7–17 steuert die Übergabe der korrekten Compiler-Flags mittels des target_compile_options()-Befehls. Blicken wir nun in die main.cpp-Datei:

#include <iostream>

int main() {
  int unused;
  
  std::cout << "Hello Reader" << std::endl;

  return 0;
}

In Zeile 4 wird die Integer-Variable unused definiert, die im weiteren Verlauf nicht verwendet wird. Der CMake-Aufruf und die spätere Ausgabe der ausführbaren Datei warning_test sind an dieser Stelle nicht von Bedeutung. Interessant ist jedoch der Aufruf des Compilers, hier beispielhaft mittels make auf Ubuntu 20.04:

$ make
Scanning dependencies of target warning_test
[ 50%] Building CXX object CMakeFiles/warning_test.dir/main.cpp.o
/blog_code/CMake/cmake_compiler_flags/Beispiel_3/main.cpp: In function ‘int main()’:
/blog_code/CMake/cmake_compiler_flags/Beispiel_3/main.cpp:4:7: warning: unused variable ‘unused’ [-Wunused-variable]
    4 |   int unused;
      |       ^~~~~~
[100%] Linking CXX executable warning_test
[100%] Built target warning_test

In den Zeilen 4–7 wird eine Warnung ausgegeben, dass die Variable unused nicht verwendet wird. Dies haben wir durch die Übergabe der entsprechenden Compiler-Flag -Wall oder /W4, falls der MSVC-Compiler verwendet wurde, erreicht.

Zusammenfassung

In diesem Beitrag habe ich euch verschiedene Befehle vorgestellt, mit denen Compiler-Flags übergeben werden können. CMake nimmt einem dabei häufig einen großen Teil der Arbeit und wandelt die Compiler-Flags korrekt für den jeweils verwendeten Compiler um. Nur bei Verwendung von Compiler-Flags, die nicht durch die CMake-Befehle target_compile_definitions(), target_compile_features() oder target_include_directories() abgedeckt werden, muss man selbst auf die korrekte Syntax achten.

Weitere Informationen

Den Code zu diesem Beitrag könnt ihr gerne weiter verwenden, ihr findet ihn dazu auf GitHub.

In meinem Buch „CMake für Einsteiger“ und in meinen Videos auf YouTube stelle ich dieses und weitere CMake Themen noch einmal detaillierter vor. Bei Fragen oder Anmerkungen schreibt mir gerne einen Kommentar. 🙂

Meine Webseite ist komplett werbefrei. Falls dir dieser Beitrag gefallen hat und du meine Arbeit gerne unterstützen möchtest, schau daher doch einmal auf meiner Support-Seite vorbei. Das würde mich sehr freuen :).

Wie hilfreich war dieser Beitrag?

Klicke auf die Sterne um zu bewerten!

Durchschnittliche Bewertung / 5. Anzahl Bewertungen:

Bisher keine Bewertungen! Sei der Erste, der diesen Beitrag bewertet.

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Nach oben scrollen
WordPress Cookie Plugin von Real Cookie Banner