Wie kann ich die Adresse eines Objekts zuverlässig abrufen, wenn der Operator & überlastet ist?

170

Betrachten Sie das folgende Programm:

struct ghost
{
    // ghosts like to pretend that they don't exist
    ghost* operator&() const volatile { return 0; }
};

int main()
{
    ghost clyde;
    ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( 
}

Wie bekomme ich die clydeAdresse?

Ich suche nach einer Lösung, die für alle Arten von Objekten gleich gut funktioniert. Eine C ++ 03-Lösung wäre schön, aber ich interessiere mich auch für C ++ 11-Lösungen. Vermeiden Sie nach Möglichkeit implementierungsspezifisches Verhalten.

Ich kenne die std::addressofFunktionsvorlage von C ++ 11 , bin aber nicht daran interessiert, sie hier zu verwenden: Ich möchte verstehen, wie ein Standard Library-Implementierer diese Funktionsvorlage implementieren kann.

James McNellis
quelle
41
@jalf: Diese Strategie ist akzeptabel, aber jetzt, wo ich besagten Personen in den Kopf geschlagen habe, wie kann ich ihren abscheulichen Code umgehen? :-)
James McNellis
5
@jalf Uhm, manchmal Sie müssen diesen Operator zu überlasten, und ein Proxy - Objekt zurück. Obwohl ich mir gerade kein Beispiel vorstellen kann.
Konrad Rudolph
5
@Konrad: ich auch nicht. Wenn Sie das brauchen, würde ich vorschlagen, dass eine bessere Option darin besteht, Ihr Design zu überdenken, da das Überladen dieses Operators einfach zu viele Probleme verursacht. :)
Jalf
2
@Konrad: In ungefähr 20 Jahren C ++ - Programmierung habe ich einmal versucht, diesen Operator zu überlasten. Das war ganz am Anfang dieser zwanzig Jahre. Oh, und ich habe es nicht geschafft, das nutzbar zu machen. Infolgedessen lautet der FAQ-Eintrag zum Überladen des Bedieners "Die unäre Adresse des Bedieners sollte niemals überladen werden." Wenn wir uns das nächste Mal treffen, erhalten Sie ein Freibier, wenn Sie ein überzeugendes Beispiel für die Überlastung dieses Betreibers finden können. (Ich weiß, dass Sie Berlin verlassen, damit ich dies sicher anbieten kann :))
sbi
5
CComPtr<>und CComQIPtr<>haben eine überladenoperator&
Simon Richter

Antworten:

102

Update: In C ++ 11 kann man std::addressofanstelle von verwenden boost::addressof.


Kopieren wir zuerst den Code von Boost, abzüglich der Compiler-Umgehung von Bits:

template<class T>
struct addr_impl_ref
{
  T & v_;

  inline addr_impl_ref( T & v ): v_( v ) {}
  inline operator T& () const { return v_; }

private:
  addr_impl_ref & operator=(const addr_impl_ref &);
};

template<class T>
struct addressof_impl
{
  static inline T * f( T & v, long ) {
    return reinterpret_cast<T*>(
        &const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
  }

  static inline T * f( T * v, int ) { return v; }
};

template<class T>
T * addressof( T & v ) {
  return addressof_impl<T>::f( addr_impl_ref<T>( v ), 0 );
}

Was passiert, wenn wir einen Verweis auf die Funktion übergeben ?

Hinweis: addressofKann nicht mit einem Zeiger auf die Funktion verwendet werden

Wenn in C ++ void func();deklariert ist, funcist dies ein Verweis auf eine Funktion, die kein Argument akzeptiert und kein Ergebnis zurückgibt. Dieser Verweis auf eine Funktion kann trivial in einen Zeiger auf eine Funktion umgewandelt werden - von @Konstantin: Nach 13.3.3.2 sind beide T &und T *für Funktionen nicht zu unterscheiden. Die erste ist eine Identitätskonvertierung und die zweite ist eine Funktion-zu-Zeiger-Konvertierung, die beide den Rang "Exakte Übereinstimmung" haben (13.3.3.1.1, Tabelle 9).

Der Verweis auf die Funktion geht durch addr_impl_ref, es gibt eine Mehrdeutigkeit in der Überlastungsauflösung für die Auswahl von f, die dank des Dummy-Arguments gelöst wird, das eine Premiere 0ist intund zu einer long(Integral Conversion) befördert werden könnte.

Wir geben also einfach den Zeiger zurück.

Was passiert, wenn wir einen Typ mit einem Konvertierungsoperator übergeben?

Wenn der Konvertierungsoperator a ergibt T*, haben wir eine Mehrdeutigkeit: Für f(T&,long)das zweite Argument ist eine integrale Promotion erforderlich, während für f(T*,int)den Konvertierungsoperator das erste aufgerufen wird (danke an @litb).

Das ist , wenn addr_impl_refTritten an. Die C ++ Standard - Aufträge , daß eine Konvertierungssequenz höchstens einen benutzerdefinierten Umwandlung enthalten. Indem addr_impl_refwir den Typ einschließen und die Verwendung einer Konvertierungssequenz bereits erzwingen, "deaktivieren" wir jeden Konvertierungsoperator, mit dem der Typ geliefert wird.

Somit wird die f(T&,long)Überlastung ausgewählt (und die integrale Förderung durchgeführt).

Was passiert bei einem anderen Typ?

Somit wird die f(T&,long)Überladung ausgewählt, da dort der Typ nicht mit dem T*Parameter übereinstimmt .

Hinweis: Aus den Anmerkungen in der Datei zur Borland-Kompatibilität geht hervor, dass Arrays nicht in Zeiger zerfallen, sondern als Referenz übergeben werden.

Was passiert bei dieser Überlastung?

Wir möchten vermeiden operator&, dass der Typ angewendet wird, da er möglicherweise überladen ist.

Der Standard garantiert, dass reinterpret_castfür diese Arbeit verwendet werden kann (siehe Antwort von @Matteo Italia: 5.2.10 / 10).

Boost fügt einige Feinheiten mit constund volatileQualifikationsmerkmalen hinzu, um Compiler-Warnungen zu vermeiden (und verwendet a ordnungsgemäß const_cast, um sie zu entfernen).

  • Besetzung T&zuchar const volatile&
  • Zieh das constund ausvolatile
  • Wenden Sie den &Operator an, um die Adresse zu übernehmen
  • Wirf zurück zu a T*

Das const/ volatileJonglieren ist ein bisschen schwarze Magie, aber es vereinfacht die Arbeit (anstatt 4 Überladungen bereitzustellen). Beachten Sie, dass, da Tunqualifiziert ist, wenn wir a bestehen ghost const&, dies der Fall T*ist ghost const*, die Qualifikanten nicht wirklich verloren gegangen sind.

BEARBEITEN: Die Zeigerüberladung wird für den Zeiger auf Funktionen verwendet, ich habe die obige Erklärung etwas geändert. Ich verstehe immer noch nicht, warum es notwendig ist .

Die folgende Ideone-Ausgabe fasst dies etwas zusammen.

Matthieu M.
quelle
2
"Was passiert, wenn wir einen Zeiger übergeben?" Teil ist falsch. Wenn wir einen Zeiger auf einen Typ U übergeben, wird die Adresse der Funktion vom Typ 'T' als 'U *' abgeleitet, und addr_impl_ref hat zwei Überladungen: 'f (U * &, long)' und 'f (U **, int) ', offensichtlich wird der erste ausgewählt.
Konstantin Oznobihin
@Konstantin: Richtig, ich hatte gedacht, dass die beiden fÜberladungen Funktionsvorlagen sind, während sie reguläre Mitgliedsfunktionen einer Vorlagenklasse sind, danke für den Hinweis. (Jetzt muss ich nur noch herausfinden, wozu die Überlastung gut ist, irgendein Tipp?)
Matthieu M.
Dies ist eine großartige, gut erklärte Antwort. Ich dachte mir, dass das ein bisschen mehr ist als nur "durchgeworfen char*". Danke, Matthieu.
James McNellis
@ James: Ich hatte viel Hilfe von @Konstantin, der mir jedes Mal, wenn ich einen Fehler machte, mit einem Stock auf den Kopf schlug: D
Matthieu M.
3
Warum sollte es Typen umgehen müssen, die eine Konvertierungsfunktion haben? Würde es nicht die genaue Übereinstimmung dem Aufrufen einer Konvertierungsfunktion vorziehen T*? EDIT: Jetzt verstehe ich. Es würde, aber mit dem 0Argument würde es in einem Kreuz enden , wäre also mehrdeutig.
Johannes Schaub - litb
99

Verwenden Sie std::addressof.

Sie können sich vorstellen, hinter den Kulissen Folgendes zu tun:

  1. Interpretieren Sie das Objekt als Referenz auf Zeichen neu
  2. Nehmen Sie die Adresse davon (wird die Überladung nicht anrufen)
  3. Wirf den Zeiger zurück auf einen Zeiger deines Typs.

Bestehende Implementierungen (einschließlich Boost.Addressof) tun genau das, indem sie sich nur um zusätzliche Pflege constund volatileQualifizierung kümmern .

Konrad Rudolph
quelle
16
Ich mag diese Erklärung besser als die ausgewählte, da sie leicht zu verstehen ist.
Schlitten
49

Der Trick dahinter boost::addressofund die Implementierung von @Luc Danton beruhen auf der Magie des reinterpret_cast; Der Standard besagt ausdrücklich in §5.2.10 ¶10, dass

Ein lvalue-Ausdruck vom Typ T1kann in den Typ "Verweis auf T2" umgewandelt werden, wenn ein Ausdruck vom Typ "Zeiger auf T1" mit a explizit in den Typ "Zeiger auf T2" konvertiert werden kann reinterpret_cast. Das heißt, eine Referenzbesetzung reinterpret_cast<T&>(x)hat den gleichen Effekt wie die Konvertierung *reinterpret_cast<T*>(&x)mit den integrierten Operatoren &und *Operatoren. Das Ergebnis ist ein l-Wert, der sich auf dasselbe Objekt wie der Quell-l-Wert bezieht, jedoch einen anderen Typ hat.

Dies ermöglicht es uns nun, eine beliebige Objektreferenz in a zu konvertieren char &(mit einer Lebenslaufqualifikation, wenn die Referenz cv-qualifiziert ist), da jeder Zeiger in a konvertiert werden kann (möglicherweise cv-qualifiziert) char *. Nachdem wir eine haben char &, ist die Überladung des Operators für das Objekt nicht mehr relevant, und wir können die Adresse mit dem eingebauten &Operator abrufen .

Die Boost-Implementierung fügt einige Schritte hinzu, um mit cv-qualifizierten Objekten zu arbeiten: Der erste Schritt reinterpret_castwird ausgeführt const volatile char &, andernfalls würde eine einfache char &Besetzung für constund / oder volatileReferenzen nicht funktionieren ( reinterpret_castkann nicht entfernt werden const). Dann wird das constund volatilemit entfernt const_cast, die Adresse mit genommen &und ein endgültiger reinterpet_castbis "korrekter" Typ erstellt.

Das const_castwird benötigt, um das const/ zu entfernen, das volatilenicht konstanten / flüchtigen Referenzen hinzugefügt werden könnte, aber es "schadet" nicht, was eine const/ volatileReferenz an erster Stelle war, da das Finale reinterpret_castdie Lebenslaufqualifikation erneut hinzufügt, wenn dies der Fall ist dort an erster Stelle ( reinterpret_castkann das nicht entfernen const, kann es aber hinzufügen).

Was den Rest des Codes betrifft addressof.hpp, so scheint es, dass das meiste davon für Problemumgehungen ist. Das static inline T * f( T * v, int )scheint nur für den Borland-Compiler erforderlich zu sein, aber sein Vorhandensein macht es erforderlich addr_impl_ref, da sonst Zeigertypen von dieser zweiten Überladung erfasst würden.

Bearbeiten : Die verschiedenen Überladungen haben unterschiedliche Funktionen, siehe @Matthieu M. ausgezeichnete Antwort .

Nun, da bin ich mir auch nicht mehr sicher. Ich sollte diesen Code weiter untersuchen, aber jetzt koche ich das Abendessen :), ich werde es mir später ansehen.

Matteo Italia
quelle
Die Erklärung von Matthieu M. bezüglich der Übergabe des Zeigers an die Adresse ist falsch.
Verwöhne
"guter Appetit", weitere Untersuchungen zeigen, dass die Überlastung als Referenz auf Funktionen aufgerufen wird void func(); boost::addressof(func);. Das Entfernen der Überladung hindert gcc 4.3.4 jedoch nicht daran, den Code zu kompilieren und dieselbe Ausgabe zu erzeugen. Daher verstehe ich immer noch nicht, warum diese Überladung erforderlich ist .
Matthieu M.
@Matthieu: Es scheint ein Fehler in gcc zu sein. Nach 13.3.3.2 sind sowohl T & als auch T * für Funktionen nicht zu unterscheiden. Die erste ist eine Identitätskonvertierung und die zweite ist eine Funktion-zu-Zeiger-Konvertierung, die beide den Rang "Exakte Übereinstimmung" haben (13.3.3.1.1, Tabelle 9). Es ist also notwendig, zusätzliche Argumente zu haben.
Konstantin Oznobihin
@Matthieu: Habe es gerade mit gcc 4.3.4 ( ideone.com/2f34P ) versucht und bin wie erwartet mehrdeutig geworden . Haben Sie überladene Mitgliedsfunktionen wie die Adresse der Implementierung oder kostenlose Funktionsvorlagen ausprobiert? Letzteres (wie ideone.com/vjCRs ) führt dazu, dass aufgrund der Regeln für den Abzug von Vorlagenargumenten (14.8.2.1/2) eine T * -Überladung ausgewählt wird.
Konstantin Oznobihin
2
@curiousguy: Warum denkst du sollte es? Ich habe auf bestimmte C ++ - Standardteile verwiesen, die vorschreiben, was der Compiler tun soll, und alle Compiler, auf die ich Zugriff habe (einschließlich, aber nicht beschränkt auf gcc 4.3.4, comeau-online, VC6.0-VC2010), melden Mehrdeutigkeiten, wie ich beschrieben habe. Könnten Sie bitte Ihre Argumentation zu diesem Fall erläutern?
Konstantin Oznobihin
11

Ich habe eine Implementierung von addressofdo this gesehen:

char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);

Fragen Sie mich nicht, wie konform das ist!

Luc Danton
quelle
5
Legal. char*ist die aufgeführte Ausnahme zum Eingeben von Aliasing-Regeln.
Welpe
6
@DeadMG Ich sage nicht, dass dies nicht konform ist. Ich sage, dass Sie mich nicht fragen sollten :)
Luc Danton
1
@DeadMG Hier gibt es kein Aliasing-Problem. Die Frage ist: ist reinterpret_cast<char*>gut definiert.
Neugieriger
2
@curiousguy und die Antwort lautet ja, es ist immer erlaubt, einen beliebigen Zeigertyp auf [unsigned] char *die Objektdarstellung des Objekts zu werfen und damit zu lesen. Dies ist ein weiterer Bereich, in dem charbesondere Privilegien bestehen.
underscore_d
@underscore_d Nur weil eine Besetzung "immer erlaubt" ist, heißt das nicht, dass Sie mit dem Ergebnis der Besetzung etwas anfangen können.
Neugieriger
5

Schauen Sie sich boost :: addressof an und seine Implementierung an.

Konstantin Oznobihin
quelle
1
Der Boost-Code ist zwar interessant, erklärt jedoch nicht, wie seine Technik funktioniert (und erklärt auch nicht, warum zwei Überladungen erforderlich sind).
James McNellis
Meinst du 'statische Inline T * f (T * v, int)' Überlastung? Sieht so aus, als würde es nur für die Borland C-Problemumgehung benötigt. Der dort verwendete Ansatz ist ziemlich einfach. Die einzige subtile (nicht standardmäßige) Sache ist die Umwandlung von 'T &' in 'char &'. Obwohl Standard, erlaubt das Gießen von 'T *' nach 'char *', scheint es keine solchen Anforderungen für das Referenzgießen zu geben. Trotzdem könnte man erwarten, dass es bei den meisten Compilern genauso funktioniert.
Konstantin Oznobihin
@Konstantin: Die Überladung wird verwendet, weil für einen Zeiger addressofder Zeiger selbst zurückgegeben wird. Es ist fraglich, ob es das ist, was der Benutzer wollte oder nicht, aber es ist, wie es angegeben hat.
Matthieu M.
@ Matthieu: Bist du sicher? Soweit ich das beurteilen kann, wird jeder Typ (einschließlich Zeigertypen) in einen eingeschlossen addr_impl_ref, daher sollte die Zeigerüberladung niemals aufgerufen werden ...
Matteo Italia
1
@KonstantinOznobihin Dies beantwortet die Frage nicht wirklich, da Sie nur sagen, wo Sie nach der Antwort suchen sollen, nicht was die Antwort ist .