MISRA-C++ bietet Richtlinien und Konformität auch für neuen Sprachstandard C++20

Seite 3: Überraschende Regeln

Inhaltsverzeichnis

Regeln blind einzuhalten, weil zum Beispiel ein Regelprüfwerkzeug eine Verletzung einer Regel anzeigt, ist trotzdem selten eine gute Idee. Manchmal verhindern Regeln die Ausgangssituation einer anderen Regel. Diese erscheint daher unnötig. Solche "sinnlosen" Regeln dienen dazu, die Situation zu entschärfen, die beim bewussten Verletzen der anderen Regel entsteht. Ein bewusstes Ausschalten der Prüfung einer verlangten Regel (Deviation) kann im Kontext eines Projekts durchaus sinnvoll sein. Regeln der Kategorie "Mandatory" sind unbedingt einzuhalten. Regeln mit Empfehlungscharakter (Advisory) lassen sich mit dokumentierter Begründung für ein Projekt generell ignorieren.

Beispielsweise dürfen eigentlich verbotene rekursive Funktionen trotzdem vorkommen, wenn im Projektkontext nachgewiesen wird, dass die Rekursion nicht zum Überlauf des Aufrufstacks führt. Eine rekursiv formulierte binäre Suche könnte zum Beispiel dadurch begrenzt sein, dass die zugehörige Datenstruktur eine fixe Größe hat, deren Zweierlogarithmus deutlich kleiner ist als der verfügbare Stackbereich. Hierzu die Regel im folgenden Kasten als Beispiel. Eine solche Abweichung von einer Regel muss aber nicht nur bewusst erfolgen, sondern auch adäquat dokumentiert werden (Deviation Procedure). Das kann man im gebührenfrei verfügbaren Dokument zur MISRA-Compliance nachlesen.

Funktionen dürfen sich nicht selbst aufrufen, weder direkt noch indirekt

Kategorie: verlangt

Analyse: unentscheidbar, System

Begründung

Rekursion birgt das Risiko, den verfügbaren Platz auf dem Laufzeitstack zu überschreiten. Das kann zu ernsthaftem Fehlverhalten führen. Wenn Rekursion nicht eng kontrolliert ist, ist es nicht möglich, vor der Ausführung festzustellen, wie viel Platz für den Laufzeitstack notwendig ist.

Anmerkung des Autors: Jede Abweichung (Deviation), die eine Verletzung dieser Regel begründet, muss erklären, wie der Laufzeitstack begrenzt wird.

Ausnahme

Eine constexpr-Funktion, die nur innerhalb einer Core Constant Expression aufgerufen wird, darf rekursiv sein. (Anmerkung: Das bedeutet, die Funktion wird vom Compiler ausgeführt und nicht zur Laufzeit des Programms.)

Beispiele – Auszug

int32_t fn ( int32_t x )
{
if ( x > 0 )
{
x = x * fn ( x – 1 ); // Non-compliant
}
return x;
}
constexpr int32_t fn_4 ( int32_t x )
{
if ( x > 0 )
{
x = x * fn_4 ( x – 1 ); // Compliant by exception
}
return x;
}
constexpr int32_t n = fn_4 ( 6 ); // Core constant expression

Die MISRA-Regeln geben zwar oft konkrete Hinweise auf die Verwendung von Sprachmechanismen, aber diese allein sind nicht ausreichend für ein sicherheitskritisches Softwaresystem. Deswegen verzichtet MISRA generell darauf, bestimmte stilistische Elemente zu fordern. Jedes entsprechende Projekt definiert aus diesem Grund weitere Programmierrichtlinien, um einen konsistenten Programmcodestil zu erreichen. AUTOSAR hat beispielsweise die Regel A7-1-7, die ein bestimmtes Codelayout fordert. Dieses Layout untersagt, mehrere Deklarationen oder Anweisungen pro Codezeile zu nutzen. Diese Regel wird wie weitere ähnliche C++-Regeln in AUTOSAR nicht in das neue MISRA-C++-Regelwerk übernommen.

Der nächste Kasten zeigt ein weiteres Beispiel für eine neue Regel im MISRA-C++ an. Zum Zeitpunkt des Entstehens des Artikels, ist diese noch nicht endgültig abgestimmt, aber sie zeigt, wie neuere Spracheigenschaften berücksichtigt werden.

Referenzen auf temporäre Objekte dürfen nicht von unqualifizierten Member-Funktionen herausgegeben werden

Kategorie: verlangt

Analyse: entscheidbar, Single Translation Unit

Version: ab C++11

Vertiefung

Diese Regel bezieht sich auf Member-Funktionen, die bei ihrer Anwendung auf ein temporäres Objekt Zeiger oder Referenzen auf Dinge aus der folgenden Liste zurückgeben:

  • *this,
  • Daten-Member von *this,
  • Basisklassen-Subobjekte von *this oder
  • andere Daten, die *this verwaltet und die zusammen mit *this gelöscht werden.

Versieht man solche Member-Funktionen mit einer Referenzqualifizierung (lvalue-ref-qualifier), verhindert das, dass sie auf temporären Objekten aufgerufen werden.

Begründung

Member-Funktionen ohne Referenzqualifikation kann man auf temporären Objekten aufrufen (Anmerkung: aus historischen Kompatibilitätsgründen auch die Nicht-const-Member-Funktionen). Wenn eine solche Funktion eine Referenz oder einen Zeiger auf die "Innereien" des Objekts zurückgibt, führt die Verwendung dieses Rückgabewerts, nachdem das Objekt zerstört wurde, zu undefiniertem Verhalten.

Ausnahmen

Die Nutzung der zurückgegebenen Referenz innerhalb desselben Ausdrucks, also bevor das temporäre Objekt gelöscht wird, ist erlaubt.

Beispiele

Das folgende Beispiel zeigt, dass der vom Compiler automatisch definierte Zuweisungsoperator diese Regel verletzt. Der Operator hat keine Referenzqualifikation, liefert aber eine Referenz auf das *this-Objekt. Benutzt man die Referenz dangle nach ihrer Initialisierung, verweist sie auf ein bereits gelöschtes Objekt.

struct X{};
void f() {
X& dangle = (X{} = X{}); // Non-compliant
}

Der folgende Code zeigt die Ausnahme. Obwohl der Output-Operator (operator<<) auch eine Referenz auf *this zurückgibt, erfolgt die Nutzung innerhalb eines Ausdrucks und das temporäre Objekt überlebt lange genug.

void log(std::ostream &out){
std::osyncstream{out} << "write log message atomically" << std::endl;
}

Speziell gefährdet ist das Range-for-Statement, wenn der "Range" eine Referenz auf ein temporäres Objekt ist.

void g(){
extern std::vector<std::string> make();
for (char c : make().front()) { // Non-compliant
// ...
}
}

Vor allem dieses Beispiel zeigt, wie die Nutzung der Member-Funktion front() auf dem von make() gelieferten Vektor dazu führt, dass die Schleife versucht, über einen bereits gelöschten String zu iterieren. Intern wird das Range-for-Statement wie folgt umgesetzt:

{ auto&& __range = make().front();
     ...
}

Die Referenz __range würde die Lebensdauer eines temporären Objekts auf der rechten Seite der Zuweisung verlängern. Das gelingt aber nicht, wenn dort als Ergebnis kein Wert, sondern eine Referenz herauskommt. Glücklicherweise geben moderne C++-Compiler (Clang) dafür eine entsprechende Warnung aus (-Wdangling-gsl), sodass man solche Fehler auch ohne teure statische Analysewerkzeuge vermeiden kann. Voraussetzung ist, alle entsprechenden Compilerwarnungen anzuschalten. Mindestens sollte also der Compileraufruf die Warnstufe -Wall -Werror enthalten, am besten zusätzlich -Wextra -pedantic, um sicherzugehen – andere Compiler als Clang und GCC bieten vergleichbare Optionen.

Falls einige der Extrawarnungen zu viele unbedenkliche korrekte Codestellen als Fehler melden, ist es besser, diese Warnungen separat und sichtbar im Build-System abzuschalten und ähnlich wie bei einer "Deviation" von den MISRA-Regeln mit einer Begründung zu dokumentieren. Das gilt auch für nicht sicherheitsrelevanten Code.