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

Seite 4: Implementierunabhängiges Verhalten

Inhaltsverzeichnis

Neben offensichtlichen Programmierfehlern adressiert MISRA C++ auch sogenanntes implementierungsabhängiges Verhalten (Implementation-defined Behavior). Das sind Konstrukte einer Programmiersprache, die aus Gründen der Effizienz nicht einheitlich auf allen Plattformen realisiert sind. Leider gilt das auch für die elementaren Ganzzahldatentypen int, long, short, char sowie deren entsprechende unsigned-Varianten. Heute ist es auf gewöhnlichen 64-Bit-Prozessoren üblich, dass int 32 Bit beansprucht, long ebenso und long long 64 Bit (LLP64).

Es gab Zeiten, als int nur 16 Bit für seinen Wertebereich zur Verfügung hatte, wie es heute auf kleineren Mikrocontrollern noch üblich ist. Zum Teil sind es auch Compileroptionen oder die Plattform, die die Anzahl der Bits der vordefinierten Ganzzahldatentypen und Zeiger festlegen. Damit ist es enorm schwierig, sicherzustellen, dass sich Programmcode auf verschiedenen Systemen wie der Entwickler-Workstation und dem Mikrocontroller in der ECU identisch verhält.

Einen Schritt geht MISRA-C++ in die entsprechende Richtung, indem eine Empfehlung (Advisory) vorschlägt, generell auf die eingebauten Datentypen zu verzichten und nur die im Standardheader <cstdint> definierten Ganzzahltypen mit fixer Breite, wie uint16_t, zu nutzen. Da Ganzzahlkonstanten aber ohne spezielle Markierung den Typ int bekommen, müsste man diese zuerst in den passenden kürzeren Typ per static_cast konvertieren.

Hier bietet sich an, dazu "User-defined Literal"-Operatoren (UDL) zu definieren, damit man analog zu 0xffULL für Hexadezimalkonstanten vom Typ unsigned long long auch die Typen aus <cstdint> als Konstantentypen nutzen kann – etwa 0xff_u8 für uint8_t oder 12345_i16 für int16_t. Wenn man hierbei noch zur Compilezeit prüfen möchte, ob die Konstante auch wirklich im Wertebereich für den Typ liegt, muss man entweder auf das C++20-Schlüsselwort consteval warten oder die Konvertierung der Ziffernzeichen als Template-UDL-Operator implementieren. Der Code dazu findet sich im Repository UDL4stdint des Autors auf GitHub.

Im folgenden Beispiel zu consteval kommt ein throw-Ausdruck zum Einsatz, der zu einem Compilefehler führt, wenn die Konstante nicht in den Zieltyp passt:

consteval uint8_t operator""_u8(unsigned long long val) {
   if (val <= std::numeric_limits<uint8_t>::max())
     return val;
   else
    throw "value is out of range of uint8_t"; // raise compile time error
}

Konstanten mit den von MISRA-C++ empfohlenen Typen zu definieren, ist nur ein erster Schritt, denn die "kürzeren" Varianten werden wegen der Ganzzahl-Promotion für Berechnungen zuerst nach int konvertiert. Durch den dann möglichen Überlauf mit Undefined Behavior fordert sicherer Code besondere Sorgfalt.

Sicherlich ist es besser, gar nicht erst die eingebauten Datentypen für Domänenwerte zu nutzen, sondern konsequent eigene Datentypen (keine Typaliase) für die vorkommenden Größen zu definieren (Strong Typing). Neben der Reduktion von Parameterverwechselungen, die durch die eingebauten automatischen Konvertierungen "erleichtert" werden, bietet sich die Möglichkeit, die entsprechenden erlaubten Rechenoperationen auf die sinnvollen einzuschränken. Es ergibt beispielsweise wenig Sinn, im Fahrzeug die gefahrene Distanz mit dem Verbrauch in Litern zu multiplizieren. Eine Division kann jedoch den Verbrauch pro Distanz oder die Reichweite pro Volumeneinheit berechnen. C++ bietet die Möglichkeit, das ohne zusätzlichen Laufzeit- und Speicherbedarf umzusetzen, wie der Codeausschnitt in Listing zeigt. Das zugehörige Framework PSsst (Peter’s simple strong typing) kann man auf GitHub nachvollziehen.

struct literGas : strong<double,literGas>
                , ops<literGas,Additive,Order,Out>{
  constexpr static inline auto  suffix=" l";
};

struct kmDriven : strong<double,kmDriven>
  , ScalarMultImpl<kmDriven,double>,Out<kmDriven> {
  constexpr static inline auto  prefix="driven ";
  constexpr static inline auto  suffix=" km";
};

struct literPer100km : strong<double,literPer100km>
                     , ops<literPer100km,Eq,Out>{
  constexpr static inline auto  suffix=" l/100km";
};
struct kmpl : strong<double,kmpl>, ops<kmpl,Eq,Out>{
  constexpr static inline auto  suffix=" km/l";
};
constexpr
literPer100km operator/(literGas l, kmDriven km){
    return {l.value/(km/100.0).value};
}
constexpr
kmpl operator/(kmDriven km, literGas l){
    return {km.value/l.value};
}
// kein Overhead!
static_assert(sizeof(double)==sizeof(kmDriven));

namespace myliterals {
constexpr literGas operator"" _l(long double value){
    return literGas{static_cast<literGas::value_type>(value)};
}
constexpr literGas operator"" _l(unsigned long long value){
    return literGas{static_cast<literGas::value_type>(value)};
}
constexpr kmDriven operator"" _km(long double value){
    return kmDriven{static_cast<kmDriven::value_type>(value)};
}
constexpr kmDriven operator"" _km(unsigned long long value){
    return kmDriven{static_cast<kmDriven::value_type>(value)};
}
}

literPer100km consumption(literGas l, kmDriven km) {
    return l/km;
}
kmpl efficiency(literGas l, kmDriven km) {
    return km/l;
}
void testConsumptionVSEfficiency(){
    using namespace myliterals;
    auto const l = 40_l;
    auto const km = 500_km;
    ASSERT_EQUAL(100/(l/km).value, (km/l).value);
}