MISRA-C++ bietet Richtlinien und Konformität auch für neuen Sprachstandard C++20
Seite 4: Implementierunabhängiges Verhalten
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);
}