wake-up-neo.com

Warum sind Funktionszeiger und Datenzeiger in C / C ++ nicht kompatibel?

Ich habe gelesen, dass das Konvertieren eines Funktionszeigers in einen Datenzeiger und umgekehrt auf den meisten Plattformen funktioniert, aber nicht garantiert funktioniert. Warum ist das so? Sollten nicht beide einfach Adressen in den Hauptspeicher und damit kompatibel sein?

127
gexicide

Eine Architektur muss Code und Daten nicht im selben Speicher speichern. Bei einer Harvard-Architektur werden Code und Daten in einem völlig anderen Speicher abgelegt. Die meisten Architekturen sind Von Neumann-Architekturen mit Code und Daten im selben Speicher, aber C beschränkt sich nach Möglichkeit nicht nur auf bestimmte Arten von Architekturen.

169
Dirk Holsopple

Einige Computer verfügen über separate Adressräume für Code und Daten. Auf einer solchen Hardware funktioniert es einfach nicht.

Die Sprache wurde nicht nur für aktuelle Desktop-Anwendungen entwickelt, sondern ermöglicht auch die Implementierung auf einem großen Hardwaresatz.


Es scheint, als hätte das C-Sprachkomitee nie beabsichtigt, void* um ein Zeiger auf eine Funktion zu sein, wollten sie nur einen generischen Zeiger auf Objekte.

Das C99-Grundprinzip besagt:

6.3.2.3 Zeiger
C wurde nun auf einer Vielzahl von Architekturen implementiert. Während einige dieser Architekturen einheitliche Zeiger aufweisen, die die Größe eines ganzzahligen Typs haben, kann maximal portierbarer Code keine notwendige Entsprechung zwischen verschiedenen Zeigertypen und den ganzzahligen Typen annehmen. In einigen Implementierungen können Zeiger sogar breiter als ein ganzzahliger Typ sein.

Die Verwendung von void* ("Zeiger auf void") als generischer Objektzeigertyp ist eine Erfindung des C89-Komitees. Die Annahme dieses Typs wurde durch den Wunsch angeregt, Funktionsprototypargumente anzugeben, die entweder beliebige Zeiger stillschweigend konvertieren (wie in fread) oder sich beschweren, wenn der Argumenttyp nicht genau übereinstimmt (wie in strcmp). . Über Zeiger auf Funktionen wird nichts gesagt, was mit Objektzeigern und/oder ganzen Zahlen nicht übereinstimmen könnte.

Hinweis Über Zeiger auf Funktionen wird im letzten Absatz nichts gesagt. Sie können sich von anderen Hinweisen unterscheiden, und der Ausschuss ist sich dessen bewusst.

37
Bo Persson

Für diejenigen, die sich an MS-DOS, Windows 3.1 und älter erinnern, ist die Antwort recht einfach. All dies diente zur Unterstützung mehrerer unterschiedlicher Speichermodelle mit unterschiedlichen Merkmalskombinationen für Code- und Datenzeiger.

So zum Beispiel für das Compact-Modell (kleiner Code, große Datenmengen):

sizeof(void *) > sizeof(void(*)())

und umgekehrt im Medium-Modell (großer Code, kleine Daten):

sizeof(void *) < sizeof(void(*)())

In diesem Fall hatten Sie keinen separaten Speicherplatz für Code und Datum, konnten aber dennoch keine Konvertierung zwischen den beiden Zeigern durchführen (außer bei Verwendung von nicht standardmäßigen Modifikatoren __near und __far).

Außerdem gibt es keine Garantie dafür, dass die Zeiger, auch wenn sie gleich groß sind, auf dasselbe verweisen - im DOS Small-Speichermodell werden sowohl Code als auch Daten in der Nähe von Zeigern verwendet, sie verweisen jedoch auf unterschiedliche Segmente. Wenn Sie also einen Funktionszeiger in einen Datenzeiger konvertieren, erhalten Sie keinen Zeiger, der in irgendeiner Beziehung zur Funktion steht. Daher war eine solche Konvertierung nicht sinnvoll.

30
Tomek

Zeiger auf ungültig sollen in der Lage sein, einen Zeiger auf irgendeine Art von Daten aufzunehmen - aber nicht notwendigerweise einen Zeiger auf eine Funktion. Einige Systeme stellen andere Anforderungen an Zeiger auf Funktionen als an Datenzeiger (z. B. gibt es DSPs mit unterschiedlicher Adressierung für Daten im Vergleich zu Code, mittleres Modell unter MS-DOS verwendet 32-Bit-Zeiger für Code, aber nur 16-Bit-Zeiger für Daten). .

23
Jerry Coffin

Zusätzlich zu dem, was hier bereits gesagt wurde, ist es interessant, POSIX dlsym() zu betrachten:

Der ISO C-Standard verlangt nicht, dass Zeiger auf Funktionen zu Zeigern auf Daten hin und her gewandelt werden können. Tatsächlich verlangt der ISO C-Standard nicht, dass ein Objekt vom Typ void * einen Zeiger auf eine Funktion enthält. Implementierungen, die die XSI-Erweiterung unterstützen, erfordern jedoch, dass ein Objekt vom Typ void * einen Zeiger auf eine Funktion enthalten kann. Das Ergebnis der Konvertierung eines Zeigers in eine Funktion in einen Zeiger auf einen anderen Datentyp (außer void *) ist jedoch noch nicht definiert. Beachten Sie, dass Compiler, die dem ISO C-Standard entsprechen, eine Warnung generieren müssen, wenn eine Konvertierung von einem void * -Zeiger in einen Funktionszeiger wie folgt versucht wird:

 fptr = (int (*)(int))dlsym(handle, "my_function");

Aufgrund des hier festgestellten Problems wird in einer zukünftigen Version möglicherweise eine neue Funktion zum Zurückgeben von Funktionszeigern hinzugefügt, oder die aktuelle Schnittstelle wird möglicherweise zugunsten von zwei neuen Funktionen nicht mehr unterstützt: eine, die Datenzeiger zurückgibt, und die andere, die Funktionszeiger zurückgibt.

12

C++ 11 hat eine Lösung für das seit langem bestehende Missverhältnis zwischen C/C++ und POSIX in Bezug auf dlsym(). Man kann reinterpret_cast zum Konvertieren eines Funktionszeigers in/von einem Datenzeiger, sofern die Implementierung diese Funktion unterstützt.

Aus der Norm 5.2.10 Abs. In 8 wird das Konvertieren eines Funktionszeigers in einen Objektzeigertyp oder umgekehrt bedingt unterstützt. 1.3.5 definiert "bedingt unterstützt" als "Programmkonstrukt, das von einer Implementierung nicht unterstützt werden muss".

9
David Hammen

Abhängig von der Zielarchitektur können Code und Daten in grundsätzlich inkompatiblen, physikalisch unterschiedlichen Speicherbereichen gespeichert werden.

7
Graham Borland

undefined bedeutet nicht unbedingt nicht erlaubt, es kann bedeuten, dass der Compiler-Implementierer mehr Freiheit hat, es so zu tun, wie er es möchte.

Bei einigen Architekturen ist dies beispielsweise möglicherweise nicht möglich - undefined ermöglicht es ihnen, weiterhin eine konforme C-Bibliothek zu haben, auch wenn Sie dies nicht tun können.

5
Martin Beckett

Sie können verschiedene Typen mit unterschiedlichen Platzanforderungen sein. Das Zuweisen zu Eins kann den Wert des Zeigers irreversibel aufteilen, so dass das Zurückgeben zu etwas anderem führt.

Ich glaube, dass es sich um verschiedene Typen handeln kann, da der Standard mögliche Implementierungen nicht einschränken möchte, die Platz sparen, wenn sie nicht benötigt werden oder wenn die CPU aufgrund der Größe zusätzlichen Mist für die Verwendung usw. verursachen könnte.

5
Edward Strange

Eine andere Lösung:

Unter der Annahme, dass POSIX garantiert, dass Funktions- und Datenzeiger dieselbe Größe und Darstellung haben (ich kann den Text dafür nicht finden, aber das angeführte Beispiel-OP schlägt vor, dass sie mindestens beabsichtigt sind, um diese Anforderung zu erfüllen), das Folgende sollte arbeiten:

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

Dadurch wird vermieden, dass die Aliasing-Regeln verletzt werden, indem char []repräsentation, die alle Typen aliasen darf.

Noch ein anderer Ansatz:

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

Aber ich würde den memcpy Ansatz empfehlen, wenn Sie absolut 100% korrektes C wollen.

4
R..

Die einzige wirklich portable Lösung besteht darin, dlsym nicht für Funktionen zu verwenden, sondern dlsym zu verwenden, um einen Zeiger auf Daten zu erhalten, die Funktionszeiger enthalten. Zum Beispiel in Ihrer Bibliothek:

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

und dann in deiner Bewerbung:

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

Im Übrigen ist dies ohnehin eine gute Entwurfspraxis und macht es einfach, sowohl das dynamische Laden über dlopen als auch das statische Verknüpfen aller Module auf Systemen zu unterstützen, die dynamisches Verknüpfen nicht unterstützen oder bei denen der Benutzer/Systemintegrator dies nicht wünscht dynamische Verknüpfung verwenden.

2
R..

Auf den meisten Architekturen haben Zeiger auf alle normalen Datentypen die gleiche Darstellung, sodass das Umsetzen zwischen Datenzeigertypen ein No-Op ist.

Es ist jedoch denkbar, dass Funktionszeiger eine andere Darstellung erfordern, möglicherweise sind sie größer als andere Zeiger. Wenn void * Funktionszeiger enthalten könnte, müsste die Darstellung von void * größer sein. Und alle Datenzeiger, die von/nach void * geworfen werden, müssten diese zusätzliche Kopie ausführen.

Wie bereits erwähnt, können Sie dies, wenn Sie dies benötigen, mithilfe einer Gewerkschaft erreichen. Die meisten Verwendungen von void * sind jedoch nur für Daten bestimmt. Daher wäre es mühsam, den gesamten Speicherbedarf zu erhöhen, nur für den Fall, dass ein Funktionszeiger gespeichert werden muss.

2
Barmar

Ein modernes Beispiel, in dem sich Funktionszeiger in der Größe von Datenzeigern unterscheiden können: C++ - Memberfunktionszeiger

Direkt zitiert aus https://blogs.msdn.Microsoft.com/oldnewthing/20040209-00/?p=40713/

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

Es gibt jetzt zwei mögliche this Zeiger.

Ein Zeiger auf eine Mitgliedsfunktion von Base1 kann als Zeiger auf eine Mitgliedsfunktion von Derived verwendet werden, da beide denselben this Zeiger verwenden. Aber ein Zeiger auf eine Mitgliedsfunktion von Base2 kann nicht wie besehen als Zeiger auf eine Mitgliedsfunktion von Derived verwendet werden, da der Zeiger this angepasst werden muss.

Es gibt viele Möglichkeiten, dies zu lösen. Hier ist, wie der Visual Studio-Compiler entscheidet, damit umzugehen:

Ein Zeiger auf eine Mitgliedsfunktion einer mehrfach vererbten Klasse ist eigentlich eine Struktur.

[Address of function]
[Adjustor]

Die Größe einer Zeiger-zu-Mitglied-Funktion einer Klasse, die Mehrfachvererbung verwendet, ist die Größe eines Zeigers plus die Größe eines size_t.

tl; dr: Bei Verwendung der Mehrfachvererbung kann ein Zeiger auf eine Member-Funktion (abhängig von Compiler, Version, Architektur usw.) tatsächlich als gespeichert werden

struct { 
    void * func;
    size_t offset;
}

das ist offensichtlich größer als ein void *.

1
Andrew Sun