C++ Core Guidelines: Die verbleibenden Regeln zu Quelldateien

In diesem Beitrag werden die Regeln der C++ Core Guidelines zu Quelldateien vervollständigt. In ihnen dreht es sich um Header-Dateien und Namensräume.

In Pocket speichern vorlesen Druckansicht 4 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Heute vervollständige ich die Regeln der C++ Core Guidelines zu Quelldateien. In ihnen dreht es sich um Header-Dateien und Namensräume.

Es ist nicht einfach, aus den verbleibenden Regeln zu Quelldateien eine Geschichte zu formen, denn diese besitzen nicht allzu viel Inhalt. Hier sind die Regeln:

Die erste Regel steht bereits für Best Practice.

Wenn du eine Header-Datei in einen Include-Guard verpackst, wird er nur einmal inkludiert. Dies ist ein kleines Beispiel aus den Guidelines:

// file foobar.h:
#ifndef LIBRARY_FOOBAR_H
#define LIBRARY_FOOBAR_H
// ... declarations ...
#endif // LIBRARY_FOOBAR_H

Zu Include-Guards sollten zwei Punkte im Auge behalten werden:

  1. Gib deinem Include-Guard einen eindeutigen Namen. Wenn du den Namen eines Include-Guards mehr als einmal verwendest, kann es sonst passieren, dass die Header-Datei nicht inkludiert wird.
  2. #pragma once ist eine nicht standardisierte, aber häufig verwendete Präprozessor-Direktive. Das heißt, dass die folgende Variation der Header-Datei foobar.h nicht portabel ist:
// file foobar.h:
#pragma once

// ... declarations ...

Zum Beispiel unterstützt der GCC diese Direktive seit 3.4. Die Details zur Unterstützung der #pragma once Direktive lassen sich schön auf Wikipedia nachlesen.

Zuerst einmal, was ist eine zirkuläre Abhängigkeit? Die folgenden Quelldateien besitzen eine einfache zirkuläre Abhängigkeit.

  • a.h
#ifndef LIBRARY_A_H
#define LIBRARY_A_H
#include "b.h"

class A {
B b;
};

#endif // LIBRARY_A_H
  • b.h
#ifndef LIBRARY_B_H
#define LIBRARY_B_H
#include "a.h"

class B {
A a;
};

#endif // LIBRARY_B_H
  • main.cpp
#include "a.h" 

int main() {
A a;
}

Wenn ich versuche, das Programm zu übersetzen, schlägt dies fehl.

Das Problem ist, dass es eine zirkuläre Abhängigkeit zwischen den Header-Dateien a.h und b.h gibt. Das Problem tritt dann auf, wenn a im Hauptprogramm erzeugt wird. Um ein A zu erzeugen, muss der Compiler die Größe von B kennen. Um die Größe von B zu kennen, muss er die Größe von A kennen. Dies ist aber nicht möglichen, wenn a oder b Objekte sind. Dies ist nur möglich, wenn a oder b Zeiger oder Referenzen sind.

Die einfache Lösung ist es, A in b.h oder B in a.h vorwärts zu deklarieren. Abhängig von deiner Plattform ist ihre Größe nun 32 oder 64 Bit. Dies ist die veränderte Header-Datei a.h.

#ifndef LIBRARY_A_H
#define LIBRARY_A_H

class B;

class A {
B* b;
B& b2 = *b;
};

#endif // LIBRARY_A_H

Eine kleine Information. Der Standard-Header <iosfwd> besitzt Vorwärtsdeklarationen der Ein- und Ausgabebibliotheken.

In die nächste Falle bin ich schon ein paar Mal gefallen.

Zum Beispiel lässt sich das folgende Programm mit dem GCC 5.4 übersetzen, hingegen schlägt es mit dem Microsoft-Compiler 19.00.23506 fehl:

#include <iostream>

int main(){

std::string s = "Hello World";
std::cout << s;

}

Ich habe in dem Programm die notwendige Header-Datei <string> vergessen. Der GCC 5.5 inkludiert automatisch die Header-Datei <string> mit der Header-Datei <iostream>. Die gilt aber nicht für den Microsoft-Compiler. Seine Fehlermeldung ist sehr wortreich:

Die nächste Regel ist kurz und prägnant.

Eine in sich abgeschlossene (self-contained) Header-Datei kann an erster Stelle in einer Übersetzungseinheit verwendet werden. Das heißt, dass sie nicht von Header-Dateien abhängt, die zuerst inkludiert werden müssen. Wenn du dieser Regel nicht folgst, kann dies einen Nutzer deiner Header-Datei sehr überraschen, da er seltsame Fehlermeldungen erhält. Zeitweise verhält sich die Header-Datei anständig; zeitweise nicht. Dies hängt davon ab, welche Header-Dateien davor verwendet wurden.

In den letzten drei Regeln geht es um Namensräume. Es geht mit einer allgemeinen Regel los.

Klar, wir besitzen wir Namensräume im C++-Standard, die logische Strukturen vorgeben. Beispiel? Hier sind ein paar:

std
std::chrono
std::literals
std::literals::chrono_literals
std::filesystem
std::placeholders

std::view // C++20

Die nächste zwei Reglen handeln von unbenannten (anonymen) Namensräumen.

Ein unbenannter Namensraum besitzt interne Bindung (internal linkage). Das heißt, dass Namen in dem unbenannten Namensraum nur innerhalb der aktuellen Übersetzungseinheit angesprochen werden können und nicht exportiert werden (SF22). Was heißt dass?

namespace {
int i; // defines ::(unique_name)::i
}
void inc() {
i++; // increments ::(unique_name)::i
}

Wenn du dich innerhalb der Übersetzungseinheit auf i beziehst, wird ein unique_name angewandt, ohne dass es zu einer Namenskollision kommt. Zum Beispiel kannst du daher eine freie Funktion add in dem unbenannten Namensraum definieren, ohne dass sich der Linker darüber beschwert, dass du die One-Definition-Rule verletzt hast.

Jetzt beziehe ich mich auf das Problem, das entsteht, wenn ein unbenannter Namensraum in der Header-Datei verwendet wird (SF21). In diesem Fall definiert jede Übersetzungseinheit ihre eindeutige Instanz des unbenannten Namensraums. Unbenannten Namensräume in Header-Dateien besitzen die folgenden Auswirkungen.

  • Das erzeugte ausführbare Datei wird größer.
  • Jede Deklaration in einem unbenannten Namensraum bezieht sich auf ein andere Entität in jeder Übersetzungseinheit. Das entspricht nicht dem erwarteten Verhalten.

Die Verwendung eines unbenannten Namensraum ist dem static Schlüsselwort in C sehr ähnlich.

namespace { int i1; }
static int i2;

Die C++ Core Guidelines haben Module in den Regeln zu Quelldateien erwähnt. Sie haben sie erwähnt, aber nicht über das neue C++20 Feature geschrieben. Dieses Lücke will ich in meinem nächsten Artikel füllen.

Ich freue mich darauf, weitere C++-Schulungen halten zu dürfen.

Die Details zu meinen C++- und Python-Schulungen gibt es auf www.ModernesCpp.de. ()