Wie berechnet man den Offset eines Klassenmitglieds zur Kompilierzeit?

Lesezeit: 7 Minuten

Wie berechnet man den Offset eines Klassenmitglieds zur Kompilierzeit
Jun Yuan

Gegeben eine Klassendefinition in C++

class A
{
  public:
    //methods definition
    ....

  private:
    int i;
    char *str;
    ....
}

Ist es möglich, den Offset eines Klassenmitglieds zur Kompilierzeit mithilfe der C++-Template-Metaprogrammierung zu berechnen? Die Klasse ist kein POD und kann virtuelle Methoden, primitive und Objektdatenelemente haben.

  • Was genau meinst du mit “Offset eines Klassenmitglieds”? Meinen Sie, wie viele Bytes Sie einem Zeiger auf eine Instanz der Klasse hinzufügen müssten (nach z. reinterpret_cast‘ing es zu char *) um zum Mitglied zu gelangen? Wenn ja, würde die einfache Subtraktion es Ihnen nicht sagen?

    – David Schwartz

    1. November ’12 um 16:02


  • Du könntest benutzen offsetof(A, i) wenn das für solche Typen definiert wäre. Sehen Sie in Ihrer Compiler-Dokumentation nach, ob dies der Fall ist.

    – Robᵩ

    1. November ’12 um 16:05


  • Hier ist der Verknüpfung zu Beispielcode, der offsetof() verwendet.

    – Hindol

    1. November ’12 um 16:08

  • @Robᵩ und Hindol: Aufgrund der erweiterten Funktionalität von Strukturen in C++ ist die Verwendung von offsetof in dieser Sprache auf “POD-Typen” beschränkt, was für Klassen mehr oder weniger dem C-Konzept von struct entspricht (obwohl nicht abgeleitet Klassen mit nur öffentlichen nicht virtuellen Memberfunktionen und ohne Konstruktor und/oder Destruktor würden ebenfalls als POD qualifizieren).

    – Murilo Vasconcelos

    1. November ’12 um 16:11

  • @MuriloVasconcelos – Einverstanden, also die Konjunktiv in meinem Satz, “wenn das war definiert.”

    – Robᵩ

    1. November ’12 um 16:34 Uhr

Wie berechnet man den Offset eines Klassenmitglieds zur Kompilierzeit
Glen Low

Basierend auf der Antwort von Matthieu M., aber kürzer und ohne Makros:

template<typename T, typename U> constexpr size_t offsetOf(U T::*member)
{
    return (char*)&((T*)nullptr->*member) - (char*)nullptr;
}

Und es heißt so:

struct X { int a, b, c, d; }

std::cout << "offset of c in X == " << offsetOf(&X::c);

Bearbeiten:

Jason Reis hat recht. Dadurch wird in C++11 kein tatsächlicher konstanter Ausdruck erzeugt. Es sieht nicht möglich aus angesichts der Einschränkungen in http://en.cppreference.com/w/cpp/language/constant_expression — insbesondere kein Zeigerunterschied und reinterpret_castkann in einem konstanten Ausdruck sein.

  • Nur um es auszudrücken, U ist der Typ des Members und T ist der Typ der Struktur. Die habe ich beim ersten Mal gefälscht.

    – Jason Reis

    10. Januar ’15 um 22:37

  • @Glen Low: Brauchen wir das wirklich? - (char*)nullptr Teil? Ziehen wir nicht einfach Null ab? Ich denke, wir können es kürzer machen 🙂

    – Navi

    9. Februar ’15 um 20:14 Uhr


  • @nav Das scheint auf gcc zu funktionieren, aber nicht auf llvm: Fehler: Rückgabeobjekt vom Typ ‘size_t’ (auch bekannt als ‘unsigned int’) kann nicht mit einem Rvalue vom Typ ‘char *’ initialisiert werden

    – Jason Reis

    26. Februar ’15 um 17:26

  • Wenn ich das obige verwende, um einen constexpr zu initialisieren, wird er nicht mit clang/llvm kompiliert.

    – Jason Reis

    26. Februar ’15 um 17:39

  • Dies führt zu undefiniertem Verhalten durch Dereferenzieren von Nullzeigern und auch durch Subtrahieren von Zeigern, wenn beide Zeiger nicht auf dasselbe Objekt zeigen

    – MM

    22. März ’16 um 21:40 Uhr

Nun, in C++11 können Sie solche Offsets tatsächlich direkt mit normalen C++-Funktionen berechnen (dh ohne an einen bestimmten intrinsischen Compiler zu delegieren).

In Aktion bei Live-Arbeitsplatz:

template <typename T, typename U>
constexpr int func(T const& t, U T::* a) {
     return (char const*)&t - (char const*)&(t.*a);
}

Dies beruht jedoch auf t ein Verweis auf a . sein constexpr Instanz hier, die möglicherweise nicht auf alle Klassen zutrifft. Es verbietet nicht T von einem haben virtual Methode, noch nicht einmal ein Konstruktor, solange es a . ist constexpr Konstrukteur.

Dennoch ist dies ein ziemliches Hindernis. In unevaluierten Kontexten könnten wir tatsächlich gebrauchen std::declval<T>() um zu simulieren, ein Objekt zu haben; während Sie keine haben. Dies stellt daher keine besonderen Anforderungen an den Konstrukteur eines Objekts. Andererseits gibt es nur wenige Werte, die wir aus einem solchen Kontext extrahieren können… und sie stellen auch Probleme mit aktuellen Compilern dar… Nun, lass es uns vortäuschen!

In Aktion bei Live-Arbeitsplatz:

template <typename T, typename U>
constexpr size_t offsetof_impl(T const* t, U T::* a) {
    return (char const*)t - (char const*)&(t->*a) >= 0 ?
           (char const*)t - (char const*)&(t->*a)      :
           (char const*)&(t->*a) - (char const*)t;
}

#define offsetof(Type_, Attr_)                          
    offsetof_impl((Type_ const*)nullptr, &Type_::Attr_)

Das einzige Problem, das ich vorhersehe, ist mit virtual Vererbung aufgrund seiner Laufzeitplatzierung des Basisobjekts. Gerne werde ich mit weiteren Mängeln konfrontiert, falls vorhanden.

  • siehe auch den eingebauten offsetof macro

    – markiert

    20. Februar ’15 um 16:10 Uhr

  • @markd: In acht nehmen => Wenn type kein Standardlayouttyp ist oder wenn das Member ein statisches Datenmember oder ein Funktionsmember ist, ist das Verhalten bei der Verwendung des nicht definiert offsetof Makro wie vom C-Standard spezifiziert ist undefiniert. Mit anderen Worten, die offsetof Makro von C funktioniert nur mit C-ähnlichen Typen.

    – Matthias M.

    20. Februar ’15 um 16:14 Uhr

  • Warum verwendest du T const* und char const* statt nur T* und char*?

    – ens

    5. November ’15 um 21:15

  • @ens: um deutlich zu machen, dass die Funktion ihre Eingabe nicht ändert (nicht durch das Makro, aber sie kann ohne verwendet werden).

    – Matthias M.

    6. November ’15 um 7:16

  • @MatthieuM. Gerade von einem neuen Compiler hier gebissen 🙂 Ich denke, die Absicht der Standard-Leute besteht darin, die Offsets der Basisklassen innerhalb einer Klasse nicht konstant zu machen, aber wenn ein Feld in einer Basisklasse ist, auch wenn diese Basisklasse in einer nicht virtuellen Klasse ist funktioniert nicht. Ich kann verstehen, warum virtuelle Basisklassen überall vorhanden sind, aber für einfache Basen? Ich hatte eine alte Vorlage, in der Sie als Basisklasse den “Knoten” für eine Liste (ein aufdringlicher Knoten auf dem Objekt) einfügen und der generische Listencode nur den Offset zum Knoten benötigt. Kann das leider nicht 😐

    – peterk

    31. Dezember ’18 um 21:28


Nein, nicht generell.

Das Makro offsetof existiert für POD-Strukturen (Plain Old Data) und kann mit C++0x leicht auf Standard-Layout-Strukturen (oder andere ähnliche geringfügige Erweiterungen) erweitert werden. Für diese eingeschränkten Fälle haben Sie also eine Lösung.

C++ bietet Compiler-Autoren viele Freiheiten. Ich kenne keine Klausel, die verhindern würde, dass einige Klassen variable Offsets zu Mitgliedern der Klasse haben – ich bin mir jedoch auch nicht sicher, warum ein Compiler dies tun würde. 😉

Ein Ansatz, um Ihren Code-Standards konform zu halten und dennoch Offsets zu haben, wäre, Ihre Daten in eine POD-Unterstruktur (oder eine C++0x-Erweiterung) zu stecken, auf der offsetof funktioniert, und dann an diesem Unterbau zu arbeiten. struct statt auf die gesamte Klasse. Oder Sie könnten auf die Einhaltung von Standards verzichten. Der Offset Ihrer Struktur innerhalb Ihrer Klasse wäre nicht bekannt, aber der Offset des Members innerhalb der Struktur.

Eine wichtige Frage ist: “Warum will ich das und habe ich wirklich einen guten Grund”?

  • Dies mag kein wirklich guter Grund sein, aber einige Datenbankinteraktionen erfordern konstante Offsets zur Kompilierzeit in Datenstrukturen, um Indizes zu erstellen. Es ist möglich, diese hart zu codieren, aber sie stattdessen vom Compiler berechnen zu lassen, kann einige Fehler vermeiden.

    – Austin Mullins

    28. Apr. ’16 um 20:15

  • @austin Das ist ein Fall für einfache alte Daten: Eine nicht standardmäßige Layoutklasse auf diese Weise in eine Datenbank zu stecken, ist eine schlechte Idee.

    – Yakk – Adam Nevraumont

    28. Apr. ’16 um 22:43

In dem 1996 erschienenen Buch “Inside the C++ Object Model”, das von Stanley B. Lippman, einem der ursprünglichen C++-Designer, geschrieben wurde, verweist er in Kapitel 4.4 auf Pointer-to-Member-Funktionen

Der zurückgegebene Wert, wenn die Adresse eines nicht statischen Datenmembers genommen wird, ist der Bytewert der Position des Members im Klassenlayout (plus 1). Man kann es sich als unvollständigen Wert vorstellen. Es muss an die Adresse eines Klassenobjekts gebunden werden, bevor auf eine tatsächliche Instanz des Members zugegriffen werden kann.

Obwohl ich mich vage an +1 aus irgendeinem früheren Leben erinnere, habe ich diese Syntax noch nie zuvor gesehen oder verwendet.

class t
{
public:
    int i;
    int j;
};
int (t::*pmf)() = &t::i;

Zumindest der Beschreibung nach scheint dies eine coole Möglichkeit zu sein, “fast” den Offset zu bekommen.

Aber es scheint zumindest in GCC nicht mehr zu funktionieren. Ich bekomme ein

Cannot initialize a variable of type 'int (t::*) with an rvalue of type "int t:: *'

Hat jemand eine Geschichte hinter dem, was hier vor sich geht? Ist so etwas noch möglich?

Problem mit dem Internet – veraltete Bücher sterben nie…

.

222710cookie-checkWie berechnet man den Offset eines Klassenmitglieds zur Kompilierzeit?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy