c systems notebook

Umbrella-Header

Manchmal werden mehrere Header häufig gemeinsam eingebunden, etwa weil sie thematisch zusammengehören oder sich gegenseitig ergänzen. In solchen Fällen kann es sinnvoll sein, sie in einer gemeinsamen Datei zusammenzufassen. Eine solche Sammeldatei nennt man Umbrella-Header.

Man kann dieses Prinzip unterschiedlich weit treiben:

  • Für einzelne Module oder Themenbereiche (z. B. ›Netzwerk‹, ›Mathematik‹ oder ›Utility‹) lässt sich je ein eigener Umbrella-Header anlegen.
  • Oder man bündelt gleich alle Standard-Header in einem großen Sammelheader, etwa für die C-Standardbibliothek.

Ein solcher Header könnte zum Beispiel standard-library.h heißen und alle Standard-Header der C-Standardbibliothek einbinden. Dann genügt künftig ein einziger

#include "standard-library.h"

So ein Header könnte etwa so aussehen:

// standard-library.h — Umbrella-Header für die C-Standardbibliothek (C23)
#ifndef STANDARD_LIBRARY_H
#define STANDARD_LIBRARY_H

#include <assert.h>
#include <complex.h>
#include <ctype.h>
#include <errno.h>
#include <fenv.h>
#include <float.h>
#include <inttypes.h>
#include <limits.h>
#include <locale.h>
#include <math.h>
#include <setjmp.h>
#include <signal.h>
#include <stdalign.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdbit.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tgmath.h>
#include <threads.h>
#include <time.h>
#include <uchar.h>
#include <wchar.h>
#include <wctype.h>

#endif // STANDARD_LIBRARY_H

Mit einem einfachen #include hätte das Programm dann Zugriff auf alle Funktionen der C-Standardbibliothek.

Allerdings hat dieses Vorgehen mehrere Nachteile:

  • Längere Kompilationszeiten: Jede Übersetzungseinheit muss alle Header einlesen, auch wenn sie nur wenige davon nutzt. Das verlängert die Build-Zeit unnötig, insbesondere bei großen Projekten ohne Precompiled Headers.

  • Namespace-Pollution: Neben Funktionen und Typen definieren Standard-Header auch zahlreiche Makros (z. B. errno, assert(), NULL, EOF). Wenn man alle Header pauschal einbindet, gelangen Symbole in den Namensraum, die man in diesem Programm sonst nie gebraucht hätte. Dadurch steigt das Risiko, dass eigene Funktionsnamen, Makros oder Variablen versehentlich mit Definitionen kollidieren, die aus Headern stammen, die man normalerweise gar nicht eingebunden hätte.

  • Verlust von Transparenz: Der Quelltext zeigt nicht mehr, welche Bibliotheken tatsächlich benötigt werden. Das erschwert Wartung und Analyse. Werkzeuge wie IWYU (Include-What-You-Use) verfolgen bewusst den entgegengesetzten Ansatz und helfen, unnötige Includes zu entfernen.

  • Portabilitätsprobleme: Nicht alle Header sind auf allen Plattformen verfügbar (z. B. fehlt <stdbit.h> in älteren Umgebungen oder Toolchains). Ein universeller Umbrella-Header müsste daher mit bedingter Kompilierung arbeiten und über Präprozessordefines prüfen, welche Header vorhanden sind. So ließe sich sicherstellen, dass nur die tatsächlich verfügbaren Header eingebunden werden, etwa durch Abfragen wie +__STDC_VERSION__+ oder herstellerspezifische Merkmale. Ohne solche Prüfungen wäre der Header nicht auf allen Systemen kompilierbar.

Wendet man das Prinzip auf eigene Header an, treten zusätzliche Probleme auf:

  • Erhöhte Abhängigkeiten: Eine Änderung in einem eingebundenen Header erzwingt eine Neukompilierung aller Quelldateien. Bei Standard-Headern ist das unkritisch, bei Projektheadern kann es Builds deutlich verlangsamen.

  • Schwächere Modularität: Module, die eigentlich unabhängig sein sollten, werden durch den gemeinsamen Umbrella-Header implizit gekoppelt.

Umbrella-Header lohnen sich daher selten. Für produktiven Code gilt: Nur das inkludieren, was tatsächlich benötigt wird.