Warum können Sie in C ++ nicht die Adresse eines Konstruktors übernehmen?

13

Gibt es einen bestimmten Grund, dass dies die Sprache konzeptionell brechen würde, oder einen bestimmten Grund, dass dies in einigen Fällen technisch nicht durchführbar ist?

Die Nutzung wäre mit neuem Betreiber.

Edit: Ich werde die Hoffnung aufgeben, meinen "neuen Operator" und "neuen Operator" klar zu machen und direkt zu sein.

Der Punkt der Frage ist: Warum sind Konstrukteure etwas Besonderes ? Denken Sie natürlich daran, dass die Sprachspezifikationen uns sagen, was legal, aber nicht unbedingt moralisch ist. Was legal ist, wird in der Regel dadurch bestimmt, was logisch mit dem Rest der Sprache übereinstimmt, was einfach und prägnant ist und was von Compilern implementiert werden kann. Die mögliche Begründung des Normungsausschusses, diese Faktoren abzuwägen, ist bewusst und interessant - daher die Frage.

Praxeolitische
quelle
Es wäre kein Problem, die Adresse eines Konstruktors zu übernehmen, sondern den Typ weiterzugeben. Vorlagen können das.
Euphoric
Was ist, wenn Sie eine Funktionsvorlage haben, die Sie mit einem Konstruktor als Argument für die Funktion erstellen möchten?
Praxeolitic
1
Es wird Alternativen für jedes Beispiel geben, das ich mir ausdenken kann, aber warum sollten Konstrukteure trotzdem etwas Besonderes sein? Es gibt viele Dinge, die Sie in den meisten Programmiersprachen wahrscheinlich nicht verwenden werden, aber solche Sonderfälle sind normalerweise gerechtfertigt.
Praxeolitic
1
@RobertHarvey Die Frage kam mir, als ich gerade eine Fabrikklasse abtippen wollte.
Praxeolitic
1
Ich frage mich , ob die C ++ 11 std::make_uniqueund std::make_sharedkann die zugrunde liegende praktische Motivation für diese Frage angemessen lösen. Hierbei handelt es sich um Vorlagenmethoden, dh, Sie müssen die Eingabeargumente im Konstruktor erfassen und dann an den eigentlichen Konstruktor weiterleiten.
Rwong

Antworten:

10

Zeiger-auf-Member-Funktionen sind nur sinnvoll, wenn Sie mehr als eine Member-Funktion mit derselben Signatur haben. Andernfalls gibt es nur einen möglichen Wert für Ihren Zeiger. Für Konstruktoren ist dies jedoch nicht möglich, da in C ++ unterschiedliche Konstruktoren derselben Klasse unterschiedliche Signaturen haben müssen.

Die Alternative für Stroustrup wäre gewesen, eine Syntax für C ++ zu wählen, bei der Konstruktoren einen anderen Namen als den Klassennamen haben könnten. Dies hätte jedoch einige sehr elegante Aspekte der vorhandenen ctor-Syntax verhindert und die Sprache komplizierter gemacht. Für mich sieht das nach einem hohen Preis aus, nur um eine selten benötigte Funktion zu ermöglichen, die leicht simuliert werden kann, indem die Initialisierung eines Objekts vom ctor auf eine andere initFunktion "ausgelagert" wird (eine normale Elementfunktion, für die Zeiger auf Elemente gelten können) erstellt).

Doc Brown
quelle
2
Trotzdem, warum verhindern memcpy(buffer, (&std::string)(int, char), size)? (Wahrscheinlich extrem koscher, aber das ist doch C ++.)
Thomas Eding
3
Entschuldigung, aber was Sie geschrieben haben, ergibt keinen Sinn. Ich sehe nichts falsch daran, Zeiger auf Member zu haben, die auf einen Konstruktor zeigen. Es hört sich auch so an, als hätten Sie etwas zitiert, ohne einen Link zur Quelle.
BЈовић
1
@ThomasEding: Was genau erwartest du von dieser Aussage? Kopieren Sie den Assembler-Code des Strings ctor irgendwo hin? Wie wird die "Größe" bestimmt (auch wenn Sie für eine Standardelementfunktion etwas Äquivalentes ausprobieren)?
Doc Brown
Ich würde erwarten, dass es dasselbe tut, was bei der Adresse eines Zeigers für freie Funktionen der Fall ist memcpy(buffer, strlen, size). Vermutlich würde es die Versammlung kopieren, aber wer weiß. Ob der Code ohne Absturz aufgerufen werden kann oder nicht, erfordert Kenntnisse über den von Ihnen verwendeten Compiler. Gleiches gilt für die Größenbestimmung. Es wäre stark plattformabhängig, aber im Produktionscode werden viele nicht portierbare C ++ - Konstrukte verwendet. Ich sehe keinen Grund, es zu verbieten.
Thomas Eding
@ThomasEding: Es wird erwartet, dass ein konformer C ++ - Compiler eine Diagnose ausgibt, wenn versucht wird, auf einen Funktionszeiger zuzugreifen, als wäre es ein Datenzeiger. Ein nicht konformer C ++ - Compiler kann alles tun, aber er kann auch eine Nicht-C ++ - Methode für den Zugriff auf einen Konstruktor bereitstellen. Dies ist kein Grund, C ++ um eine Funktion zu erweitern, die sich nicht für die Anpassung von Code eignet.
Bart van Ingen Schenau
5

Ein Konstruktor ist eine Funktion, die Sie aufrufen, wenn das Objekt noch nicht vorhanden ist. Es kann sich also nicht um eine Mitgliedsfunktion handeln. Es könnte statisch sein.

Ein Konstruktor wird tatsächlich mit diesem Zeiger aufgerufen, nachdem der Speicher zugewiesen wurde, aber bevor er vollständig initialisiert wurde. Infolgedessen verfügt ein Konstruktor über eine Reihe von privilegierten Funktionen.

Wenn Sie einen Zeiger auf einen Konstruktor hätten, müsste es sich entweder um einen statischen Zeiger handeln, etwa um eine Factory-Funktion, oder um einen speziellen Zeiger auf etwas, das unmittelbar nach der Speicherzuweisung aufgerufen wird. Es konnte keine gewöhnliche Member-Funktion sein und trotzdem als Konstruktor arbeiten.

Der einzige nützliche Zweck, der in den Sinn kommt, ist eine spezielle Art von Zeiger, der an den neuen Operator übergeben werden kann, damit dieser indirekt auf den zu verwendenden Konstruktor zugreifen kann. Ich denke, das könnte praktisch sein, aber es würde eine bedeutende neue Syntax erfordern, und vermutlich lautet die Antwort: Sie haben darüber nachgedacht und es war die Mühe nicht wert.

Wenn Sie nur den allgemeinen Initialisierungscode umgestalten möchten, ist eine normale Speicherfunktion normalerweise eine ausreichende Antwort, und Sie können einen Zeiger auf eine dieser Funktionen erhalten.

david.pfx
quelle
Dies scheint die richtigste Antwort zu sein. Ich erinnere mich an einen Artikel aus vielen (vielen) Jahren, der sich mit dem neuen Bediener und den internen Abläufen des „neuen Bedieners“ befasste. Der Operator new () weist Speicherplatz zu. Der neue Operator ruft den Konstruktor mit dem zugewiesenen Speicherplatz auf. Das Aufrufen der Adresse eines Konstruktors ist etwas Besonderes, da der Aufruf des Konstruktors Platz erfordert. Der Zugriff zum Aufrufen eines Konstruktors wie diesem erfolgt mit der Platzierung new.
Bill Door
1
Das Wort "exist" verdeckt das Detail, dass ein Objekt eine Adresse haben und Speicher zugewiesen haben kann, aber nicht initialisiert werden kann. Ich denke, dass das Erhalten des this-Zeigers eine Funktion zu einer Member-Funktion macht, da sie eindeutig einer Objektinstanz zugeordnet ist (auch wenn sie nicht initialisiert ist). Die Antwort wirft jedoch einen guten Punkt auf: Der Konstruktor ist die einzige Member-Funktion, die für ein nicht initialisiertes Objekt aufgerufen werden kann.
Praxeolitic
Egal, anscheinend haben sie die Bezeichnung "spezielle Mitgliedsfunktionen". Klausel 12 des C ++ 11-Standards: "Der Standardkonstruktor (12.1), der Kopierkonstruktor und der Kopierzuweisungsoperator (12.8), der Verschiebungskonstruktor und der Verschiebungszuweisungsoperator (12.8) sowie der Destruktor (12.4) sind spezielle Elementfunktionen ."
Praxeolitic
Und 12.1: "Ein Konstruktor darf nicht virtuell (10.3) oder statisch (9.4) sein." (meine Betonung)
Praxeolitic
1
Tatsache ist, dass wenn Sie mit Debug-Symbolen kompilieren und nach einem Stack-Trace suchen, tatsächlich ein Zeiger auf den Konstruktor vorhanden ist. Was ich nie finden konnte, ist die Syntax, um diesen Zeiger zu erhalten ( &A::Afunktioniert in keinem der Compiler, die ich ausprobiert habe.)
AlfC
-3

Dies liegt daran, dass es sich nicht um einen Rückgabetyp für einen Konstruktor handelt und Sie keinen Speicherplatz für den Konstruktor im Speicher reservieren. Wie u im Falle einer Variablen während der Deklaration. Zum Beispiel: Wenn Sie eine einfache Variable X schreiben, generiert der Compiler einen Fehler, da der Compiler die Bedeutung dieses Fehlers nicht versteht. Aber wenn Sie Int x schreiben; Dann erfährt der Compiler, dass es sich um eine Variable vom Typ int handelt, sodass Platz für Variablen reserviert wird.

Schlussfolgerung: - Die Schlussfolgerung ist, dass aufgrund des Ausschlusses des Rückgabetyps die Adresse nicht im Speicher abgerufen wird.

liebevoller Goyal
quelle
1
Der Code im Konstruktor muss eine Adresse im Speicher haben, weil er irgendwo sein muss. Es ist nicht erforderlich, Speicherplatz auf dem Stapel zu reservieren , er muss sich jedoch irgendwo im Speicher befinden. Sie können die Adresse von Funktionen übernehmen, die keine Werte zurückgeben. (void)(*fptr)()deklariert einen Zeiger auf eine Funktion ohne Rückgabewert.
Praxeolitic
2
Sie haben den Punkt der Frage verpasst - in dem ursprünglichen Beitrag wurde nach der Adresse des Codes für den Konstruktor gefragt, nicht nach dem Ergebnis, das der Konstruktor bereitgestellt hat. Verwenden Sie in diesem Forum außerdem vollständige Wörter: "u" ist kein akzeptabler Ersatz für "Sie".
BobDalgleish
Herr Praxeolitic, ich denke, wenn wir keinen Rückgabetyp erwähnen, wird der Compiler keinen bestimmten Speicherort für ctor festlegen und dieser Speicherort wird intern festgelegt. Können wir die Adresse von irgendetwas in c ++ abrufen, das nicht von gegeben ist? Compiler? Wenn ich falsch
liege,
Und erzähl mir auch von Referenzvariablen. Können wir die Adresse der Referenzvariablen abrufen? Wenn nein, welche Adresse printf ("% u", & (& (j))); wird gedruckt, wenn & j = x, wobei x = 10 ist? Weil die von printf gedruckte Adresse und die Adresse von x nicht gleich sind
Goyal
-4

Ich nehme eine wilde Vermutung an:

C ++ - Konstruktor und Destruktor sind überhaupt keine Funktionen, sondern Makros. Sie werden in den Bereich eingefügt, in dem das Objekt erstellt wird, und in den Bereich, in dem das Objekt zerstört wird. Es gibt wiederum keinen Konstruktor oder Destruktor, das Objekt ist nur IS.

Tatsächlich denke ich, dass die anderen Funktionen in der Klasse keine Funktionen sind, sondern Inline-Funktionen, die NICHT inline gesetzt werden, weil Sie die Adresse von ihnen übernehmen (der Compiler erkennt, dass Sie darauf zugreifen und den Code nicht in die Funktion und inline setzen optimiert diese Funktion) und wiederum scheint die Funktion "immer noch da" zu sein, obwohl dies nicht der Fall wäre, wenn Sie die Adresse nicht angegeben hätten.

Die virtuelle Tabelle des C ++ - "Objekts" ist nicht wie ein JavaScript-Objekt, über das Sie den Konstruktor abrufen und Objekte daraus zur Laufzeit erstellen können new XMLHttpRequest.constructor, sondern vielmehr eine Sammlung von Zeigern auf anonyme Funktionen, die als Schnittstelle für dieses Objekt dienen , mit Ausnahme der Möglichkeit, das Objekt zu erstellen. Und es macht nicht einmal Sinn, das Objekt zu "löschen", denn es ist wie der Versuch, eine Struktur zu löschen. Sie können es nicht: Es ist nur eine Stapelbeschriftung. Schreiben Sie es einfach, wie Sie möchten, unter einer anderen Beschriftung benutze eine Klasse als 4 Ganzzahlen:

/* i imagine this string gets compiled into a struct, one of which's members happens to be a const char * which is initialized to exactly your string: no function calls are made during construction. */
std::string a = "hello, world";
int *myInt = (int *)(*((void **)&a));
myInt[0] = 3;
myInt[1] = 9;
myInt[2] = 20;
myInt[3] = 300;

Es gibt keinen Speicherverlust, es gibt keine Probleme, außer dass Sie effektiv eine Menge Stapelspeicherplatz verschwendet haben, der für die Objektschnittstelle und die Zeichenfolge reserviert ist, aber Ihr Programm wird nicht zerstört (solange Sie nicht versuchen, es zu verwenden) als eine Zeichenfolge immer wieder).

Wenn meine früheren Annahmen richtig sind: Die vollständigen Kosten der Zeichenfolge bestehen nur aus den Kosten für das Speichern dieser 32 Bytes und dem konstanten Zeichenfolge-Speicherplatz: Die Funktionen werden nur zur Kompilierungszeit verwendet und können auch nachher eingebettet und weggeworfen werden Das Objekt wird erstellt und verwendet (Als ob Sie mit einer Struktur gearbeitet hätten und nur direkt ohne Funktionsaufrufe darauf verwiesen hätten, stellen Sie sicher, dass es doppelte Aufrufe anstelle von Funktionssprüngen gibt, dies ist jedoch in der Regel schneller und benötigt weniger Speicherplatz). Wenn Sie eine Funktion aufrufen, ersetzt der Compiler diesen Aufruf im Wesentlichen durch die entsprechenden Anweisungen, mit Ausnahme der von den Sprachentwicklern festgelegten Ausnahmen.

Zusammenfassung: C ++ - Objekte haben keine Ahnung, was sie sind. Alle Tools für die Schnittstelle sind statisch inline und gehen zur Laufzeit verloren. Dies macht das Arbeiten mit Klassen so effizient wie das Füllen von Strukturen mit Daten und das direkte Arbeiten mit diesen Daten, ohne dass Funktionen überhaupt aufgerufen werden (diese Funktionen sind inline).

Dies unterscheidet sich grundlegend von den Ansätzen von COM / ObjectiveC sowie von Javascript, bei denen die Typinformationen dynamisch beibehalten werden. Dies geht zu Lasten des Laufzeit-Overheads, der Speicherverwaltung und der Aufrufe von Konstruktionen, da der Compiler diese Informationen nicht wegwerfen kann. Dies ist erforderlich für den dynamischen Versand. Dies gibt uns die Möglichkeit, zur Laufzeit mit unserem Programm zu "sprechen" und es im laufenden Betrieb mit reflektierbaren Komponenten zu entwickeln.

Dmitry
quelle
2
Entschuldigung, aber einige Teile dieser "Antwort" sind entweder falsch oder gefährlich irreführend. Leider ist der Kommentarbereich viel zu klein, um sie alle aufzulisten (die meisten Methoden werden nicht eingebunden, dies würde den virtuellen Versand verhindern und die Binärdatei aufblähen; selbst wenn eingebunden, könnte es eine adressierbare Kopie geben, die irgendwo zugänglich ist; Der schlimmste Fall beschädigt Ihren Stack und passt im besten Fall nicht zu Ihren Annahmen; ...)
Hoffmale
Die Antwort ist naiv, ich wollte nur meine Vermutung zum Ausdruck bringen, warum Konstruktor / Destruktor nicht referenziert werden können. Ich bin damit einverstanden, dass im Fall von virtuellen Klassen die vtable bestehen bleiben muss und der adressierbare Code im Speicher vorhanden sein muss, damit die vtable darauf verweisen kann. Klassen, die keine virtuelle Klasse implementieren, scheinen jedoch wie im Fall von std :: string inline zu sein. Nicht alles wird eingebunden, aber Dinge, die nicht minimal in einem "anonymen" Codeblock irgendwo im Speicher abgelegt zu sein scheinen. Wie beschädigt der Code den Stapel? Sicher, wir haben die Saite verloren, aber sonst haben wir sie nur neu interpretiert.
Dmitry
Eine Speicherbeschädigung tritt in einem Computerprogramm auf, wenn der Inhalt eines Speicherorts unbeabsichtigt geändert wird. Dieses Programm tut dies absichtlich und versucht nicht mehr, diese Zeichenfolge zu verwenden. Es gibt also keine Beschädigung, sondern nur verschwendeten Stapelspeicher. Aber ja, die Invariante der Zeichenfolge wird nicht mehr beibehalten, sondern überfüllt den Gültigkeitsbereich (an dessen Ende der Stapel wiederhergestellt wird).
Dmitry
Abhängig von der String-Implementierung können Sie über Bytes schreiben, die Sie nicht möchten. Wenn die Zeichenfolge so ähnlich ist struct { int size; const char * data; };(wie Sie anscheinend annehmen), schreiben Sie 4 * 4 Bytes = 16 Bytes auf eine Speicheradresse, für die Sie auf einem x86-Computer nur 8 Bytes reserviert haben, sodass 8 Bytes über andere Daten geschrieben werden (was Ihren Stapel beschädigen kann) ). Glücklicherweise gibt es std::stringnormalerweise eine direkte Optimierung für kurze Zeichenfolgen. Sie sollte daher für Ihr Beispiel ausreichend groß sein, wenn Sie eine größere Standardimplementierung verwenden.
Hoffmale
@hoffmale du hast absolut recht, es könnten 4 bytes sein, es könnten 8 oder sogar 1 byte sein. Wenn Sie jedoch die Größe der Zeichenfolge kennen, wissen Sie auch, dass sich dieser Speicher im aktuellen Bereich im Stapel befindet, und Sie können ihn nach Belieben verwenden. Mein Punkt war, dass, wenn Sie die Struktur kennen, diese unabhängig von Informationen über die Klasse gepackt ist, im Gegensatz zu COM-Objekten, deren UUID ihre Klasse als Teil der VTABELLE von IUnknown identifiziert. Der Compiler greift auf diese Daten wiederum direkt über Inlining- oder verkürzte statische Funktionen zu.
Dmitry