reinterpret_cast vs. konstanter Ausdruck

reinterpret_cast vs. konstanter Ausdruck

Als ich meine Zehen in ein neues Projekt eintauchte, erhielt ich eine Reihe hässlicher Warnungen über eine Tonne C-Casts in einer Makrodefinition. Der Versuch, ihnen zu entkommen, war nicht so einfach, wie ich zuerst dachte.

Der Compiler hat etwas mehr als 1000 Warnungen ausgegeben – genauer gesagt 1000 Mal dieselbe Warnung. Wenn man sich den fraglichen Code ansieht, wäre es so etwas Unschuldiges:

someFunc(FOO);
someOtherFunc(BAR->i);

Beide Zeilen sehen nicht wirklich so aus, als ob eine Besetzung im Gange wäre. Aber warten Sie – die Großbuchstaben FOO und BAR verdächtig aussehen. Das Finden der Definitionen hat eine Weile gedauert – wir verwenden eine IDE für die eingebettete Entwicklung, und sie ist nicht mit funktionierenden Funktionen wie „Zur Definition springen“ gesegnet.

Die Definitionen von FOO und BAR sah dann so aus:

#define FOO ((uint8*)0xBAD50BAD)
#define BAR ((S*)FOO)

Wobei uint8 ist eine Typdefinition für einen 8-Bit-Typ ohne Vorzeichen, und S ist eine Struktur. Da waren sie, die Abgüsse im C-Stil. Und um den C-Stil nicht zu brechen, verwendete der Autor dieses Codes Makros anstelle von konstanten Ausdrücken.

Um fair zu sein, eine Handvoll dieser Makros befanden sich in tatsächlichen C-Headern, die von Dritten bereitgestellt wurden, aber viele von ihnen schienen nur im gleichen Stil in einem Projekt geschrieben zu sein, das ausdrücklich behauptet, ein C++-Projekt zu sein.

Korrektur des C-Stils

Die meisten C++-Entwickler wissen, dass #define s sind „böse“, weil sie einfache Textersetzungen sind und daher Probleme wie fehlende Typsicherheit und mehr mit sich bringen.

In diesem Fall hat die Verwendung von Makros das Problem schlimmer erscheinen lassen, als es tatsächlich war:Nur ein paar Dutzend dieser Makros können zu Hunderten oder Tausenden von Warnungen führen, da der Compiler nach dem Austausch diesen C-Cast an jeder Stelle sieht Makro wird verwendet .

Wenn wir fortfahren und das Makro durch einen konstanten Ausdruck ersetzen, sollten wir die Warnung genau an der Stelle erhalten, an der der C-Cast geschrieben ist, und nicht an der Stelle, an der die Makros erweitert werden. Wenn wir schon dabei sind, können wir den C-Cast durch den richtigen C++-Cast ersetzen, der in diesem Fall reinterpret_cast ist :

constexpr auto FOO = reinterpret_cast<uint8*>(0xBAD50BAD);
constexpr auto BAR = reinterpret_cast<S*>(FOO);

Leider wird dies nicht kompiliert, weil reinterpret_cast s sind in konstanten Ausdrücken laut Standard nicht erlaubt. Bevor Sie fragen:Nein, wir können nicht zum C-Cast zurückkehren, da die Regeln in diesem Fall effektiv eine reinterpret_cast besagen durchgeführt wird.

Was können wir tun?

Wir könnten hier aufhören und aufgeben. Wir könnten einfach reinterpret_cast schreiben in den Makros und leben damit, dass wir hässliche Makros haben, aber die Warnungen zum Schweigen bringen. Aber das ist nicht zu befriedigend, oder?

Mir fällt ein, dass die eigentliche Konstante hier der Adresswert ist, also die 0xBA50BAD , und die reinterpret_cast s werden nur im Laufzeitcode verwendet. Daher möchten wir die Besetzung möglicherweise nicht in den konstanten Ausdruck einbacken.

Ein weiterer Punkt ist, dass die konstanten Zeiger relativ oft paarweise vorkommen:A unit8* das scheint für Lese- und Schreibvorgänge auf sehr niedriger Ebene im Speicher verwendet zu werden, und ein Zeiger auf denselben Ort, der die Daten als ein Objekt wie S interpretiert oben.

Wir wollen wahrscheinlich nur diese Paare, d. h. die Interpretation derselben Adresse als noch etwas anderes ist möglicherweise nicht erwünscht. Vor diesem Hintergrund stellt sich die Frage, ob wir eine Klasse entwickeln könnten, die

  • Erlaubt uns, constexpr zu verwenden statt Makros
  • Liefert einen uint8* und ein Zeiger auf einen festen anderen Typ

Eine Klassenvorlage, die diese Anforderungen erfüllt, könnte wie folgt aussehen:

template <class T> class mem_ptr{
  std::intptr_t addr; 
public:
  constexpr mem_ptr(std::intptr_t i) : addr{i} {}
  operator T*() const { return reinterpret_cast<T*>(addr); }
  T* operator->() const { return operator T*(); }

  uint8* raw() const { return reinterpret_cast<uint8*>(addr); }
};

std::intptr_t ist ein Alias ​​für einen ganzzahligen Typ, der groß genug ist, um einen Zeigerwert aufzunehmen. Da die Klasse diesen ganzzahligen Wert und keinen Zeigerwert enthält, kann sie als konstanter Ausdruck verwendet werden. Die Konvertierungen in die beiden Zeigertypen müssen noch im Laufzeitcode erfolgen, sie befinden sich also in Funktionen, die nicht constepr sind .

Um diese Klasse in der aktuellen Codebasis zu verwenden, ohne anderen Code zu berühren, bräuchten wir so etwas wie die nächsten beiden Zeilen:

constexpr auto BAR = mem_ptr<S>(0xBAD50BAD);
#define FOO BAR.raw()

Juhu, keine Casts mehr in unseren Konstanten. Das eigentliche Zeigerobjekt ist ein konstanter Ausdruck, aber wir haben immer noch ein Makro, was ist damit?

Umwandlung in `uint*`

Wir könnten weitermachen und unseren raw ersetzen Funktion mit einem impliziten Konvertierungsoperator, aber ich denke, das sollten wir nicht tun. Es würde die gleiche Konstante BAR machen konvertierbar in einen S* und ein uint8* , was ziemlich verwirrend sein kann.

Daher habe ich die Umstellung auf uint8* vorgenommen eine explizite Funktion. Ich werde verlangen, dass wir alle Vorkommen von FOO ersetzen mit dem Aufruf dieser Funktion, aber das ist aus zwei Gründen positiv:

  1. FOO und BAR waren zuvor nicht verwandt und zeigten nicht, dass sie denselben Speicher und dasselbe Objekt auf unterschiedliche Weise adressierten. Jetzt haben wir eine Konstante BAR die wir für beide Wege verwenden.
  2. Machen raw Eine explizite Funktion macht sehr deutlich, dass wir auf Rohspeicher zugreifen, was notwendig sein kann, aber eine unsichere Operation sein kann, die entsprechend gekapselt werden sollte.

Leistung

Da wir uns in einem eingebetteten Projekt befinden, sind Speicher und Leistung entscheidend. Allerdings haben wir den Umweg über den Konvertierungsoperator und den raw Die Funktion ist minimal und die Funktionsaufrufe sind auf niedrigen Optimierungsstufen eingebettet (z. B. -O1 auf ARM GCC).