Pourquoi C ++ ne vous permet-il pas de prendre l'adresse d'un constructeur?

14

Y a-t-il une raison spécifique pour laquelle cela briserait le langage conceptuellement ou une raison spécifique pour laquelle cela est techniquement irréalisable dans certains cas?

L'utilisation serait avec un nouvel opérateur.

Edit: je vais abandonner tout espoir d'obtenir mon "nouvel opérateur" et "nouvel opérateur" directement et être direct.

Le point de la question est: pourquoi les constructeurs sont-ils spéciaux ? Gardez à l'esprit bien sûr que les spécifications linguistiques nous disent ce qui est légal, mais pas nécessairement moral. Ce qui est légal est généralement informé par ce qui est logiquement cohérent avec le reste du langage, ce qui est simple et concis, et ce qui est réalisable pour les compilateurs. La justification possible du comité des normes pour évaluer ces facteurs est délibérée et intéressante - d'où la question.

Praxéolitique
la source
Ce ne serait pas un problème de prendre l'adresse d'un constructeur, mais de pouvoir faire circuler le type. Les modèles peuvent le faire.
Euphoric
Que faire si vous avez un modèle de fonction que vous souhaitez construire un objet à l'aide d'un constructeur qui sera spécifié comme argument de la fonction?
Praxeolitic
1
Il y aura des alternatives pour tout exemple que je peux imaginer, mais pourquoi les constructeurs devraient-ils être spéciaux? Il y a beaucoup de choses que vous n'utiliserez probablement pas dans la plupart des langages de programmation, mais des cas spéciaux comme celui-ci viennent généralement avec une justification.
Praxeolitic
1
@RobertHarvey La question m'est venue quand j'étais sur le point de taper une classe d'usine.
Praxeolitic
1
Je me demande si le C ++ 11 std::make_uniqueet std::make_sharedpeut résoudre adéquatement la motivation pratique sous-jacente pour cette question. Ce sont des méthodes de modèle, ce qui signifie qu'il faut capturer les arguments d'entrée au constructeur, puis les transmettre au constructeur réel.
rwong

Réponses:

10

Les fonctions pointeurs vers membre n'ont de sens que si vous avez plusieurs fonctions membres avec la même signature - sinon il n'y aurait qu'une seule valeur possible pour votre pointeur. Mais cela n'est pas possible pour les constructeurs, car en C ++, différents constructeurs de la même classe doivent avoir des signatures différentes.

L'alternative pour Stroustrup aurait été de choisir une syntaxe pour C ++ où les constructeurs pourraient avoir un nom différent du nom de la classe - mais cela aurait empêché certains aspects très élégants de la syntaxe ctor existante et aurait rendu le langage plus compliqué. Pour moi, cela ressemble à un prix élevé juste pour permettre une fonctionnalité rarement nécessaire qui peut être facilement simulée en «externalisant» l'initialisation d'un objet du ctor vers une initfonction différente (une fonction membre normale pour laquelle le pointeur vers les membres peut être créé).

Doc Brown
la source
2
Mais pourquoi éviter memcpy(buffer, (&std::string)(int, char), size)? (Probablement extrêmement non casher, mais c'est du C ++ après tout.)
Thomas Eding
3
désolé, mais ce que vous avez écrit n'a aucun sens. Je ne vois rien de mal à avoir un pointeur sur un membre pointant vers un constructeur. aussi, il semble que vous ayez cité quelque chose, sans lien vers la source.
BЈовић
1
@ThomasEding: qu'attendez-vous exactement de cette déclaration? Copie du code d'assemblage de la chaîne ctor somewgere? Comment la "taille" sera-t-elle déterminée (même si vous essayez quelque chose d'équivalent pour une fonction membre standard)?
Doc Brown
Je m'attends à ce qu'il fasse la même chose qu'il ferait en fonction de l'adresse d'un pointeur de fonction libre memcpy(buffer, strlen, size). Vraisemblablement, cela copierait l'assemblage, mais qui sait. Que le code puisse ou non être invoqué sans se bloquer, il faudrait connaître le compilateur que vous utilisez. Il en va de même pour déterminer la taille. Cela dépendrait fortement de la plate-forme, mais de nombreuses constructions C ++ non portables sont utilisées dans le code de production. Je ne vois aucune raison de l'interdire.
Thomas Eding
@ThomasEding: Un compilateur C ++ conforme devrait fournir un diagnostic lors de la tentative d'accès à un pointeur de fonction comme s'il s'agissait d'un pointeur de données. Un compilateur C ++ non conforme pourrait faire n'importe quoi, mais il peut également fournir un moyen non c ++ d'accéder à un constructeur. Ce n'est pas une raison pour ajouter une fonctionnalité au C ++ qui n'a aucune utilité dans le code conforme.
Bart van Ingen Schenau
5

Un constructeur est une fonction que vous appelez lorsque l'objet n'existe pas encore, il ne peut donc pas s'agir d'une fonction membre. Cela pourrait être statique.

Un constructeur est en fait appelé avec un pointeur this, après que la mémoire a été allouée mais avant qu'elle ne soit complètement initialisée. En conséquence, un constructeur possède un certain nombre de fonctionnalités privilégiées.

Si vous aviez un pointeur sur un constructeur, il faudrait soit un pointeur statique, quelque chose comme une fonction d'usine, soit un pointeur spécial vers quelque chose qui serait appelé immédiatement après l'allocation de mémoire. Il ne peut pas s'agir d'une fonction membre ordinaire et fonctionne toujours comme constructeur.

Le seul but utile qui me vient à l'esprit est un type spécial de pointeur qui pourrait être transmis au nouvel opérateur pour lui permettre d'indiquer indirectement le constructeur à utiliser. Je suppose que cela pourrait être pratique, mais cela nécessiterait une nouvelle syntaxe importante et probablement la réponse est: ils y ont pensé et cela n'en valait pas la peine.

Si vous souhaitez simplement refactoriser le code d'initialisation commun, une fonction de mémoire ordinaire est généralement une réponse suffisante, et vous pouvez obtenir un pointeur sur l'une d'entre elles.

david.pfx
la source
Cela semble être la réponse la plus correcte. Je me souviens d'un article d'il y a de nombreuses années concernant l'opérateur nouveau et le fonctionnement interne du «nouvel opérateur». L'opérateur new () alloue de l'espace. Le nouvel opérateur appelle le constructeur avec cet espace alloué. Prendre l'adresse d'un constructeur est «spécial» car appeler le constructeur nécessite de l'espace. L'accès pour appeler un constructeur comme celui-ci se fait avec le placement new.
Bill Door
1
Le mot "exister" masque le détail qu'un objet peut avoir une adresse et avoir de la mémoire allouée mais ne pas être initialisé. Sur la fonction membre ou non, je pense que l'obtention du pointeur this fait d'une fonction une fonction membre car elle est clairement associée à une instance d'objet (même si elle n'est pas initialisée). Cela dit, la réponse soulève un bon point: le constructeur est la seule fonction membre qui peut être appelée sur un objet non initialisé.
Praxeolitic
Peu importe, ils ont apparemment la désignation de "fonctions spéciales de membre". Article 12 de la norme C ++ 11: "Le constructeur par défaut (12.1), le constructeur de copie et l'opérateur d'affectation de copie (12.8), le constructeur de déplacement et l'opérateur d'affectation de déplacement (12.8) et le destructeur (12.4) sont des fonctions membres spéciales ."
Praxeolitic
Et 12.1: "Un constructeur ne doit pas être virtuel (10.3) ou statique (9.4)." (c'est moi qui souligne)
Praxeolitic
1
Le fait est que si vous compilez avec des symboles de débogage et recherchez une trace de pile, il y a en fait un pointeur vers le constructeur. Ce que je n'ai jamais pu trouver, c'est trouver la syntaxe pour obtenir ce pointeur ( &A::Ane fonctionne dans aucun des compilateurs que j'ai essayés.)
alfC
-3

Cela est dû au fait qu'il n'y a pas de type de retour de constructeur et que vous ne réservez aucun espace pour le constructeur en mémoire. Comme vous le faites en cas de variable lors de la déclaration. Par exemple: si u écrit une variable simple X, alors le compilateur générera une erreur car le compilateur ne comprendra pas la signification de cela. Mais lorsque vous écrivez Int x; Ensuite, le compilateur sait qu'il est de type variable de données, il réservera donc un espace pour la variable.

Conclusion: - donc la conclusion est qu'en raison de l'exclusion du type de retour, il n'obtiendra pas l'adresse en mémoire.

Lovish Goyal
la source
1
Le code dans le constructeur doit avoir une adresse en mémoire car elle doit être quelque part. Il n'est pas nécessaire de lui réserver de l'espace sur la pile, mais il doit se trouver quelque part en mémoire. Vous pouvez prendre l'adresse des fonctions qui ne renvoient pas de valeurs. (void)(*fptr)()déclare un pointeur sur une fonction sans valeur de retour.
Praxeolitic
2
Vous avez manqué le point de la question - le message d'origine demandait de prendre l'adresse du code pour le constructeur, pas le résultat fourni par le constructeur. De plus, sur ce tableau, veuillez utiliser des mots entiers: "u" n'est pas un remplacement acceptable pour "vous".
BobDalgleish
M. praxeolitic, je pense que si nous ne mentionnons aucun type de retour, le compilateur ne définira pas un emplacement de mémoire particulier pour ctor et son emplacement est défini en interne .... Pouvons-nous récupérer l'adresse de n'importe quelle chose en c ++ qui n'est pas donnée par compilateur? Si je me trompe, veuillez me corriger avec la bonne réponse
Lovish Goyal
Et parlez-moi aussi de la variable de référence. Peut-on récupérer l'adresse de la variable de référence? Si non, quelle adresse printf ("% u", & (& (j))); imprime si & j = x où x = 10? Parce que l'adresse imprimée par printf et l'adresse de x ne sont pas les mêmes
lovish Goyal
-4

Je vais faire une supposition folle:

Le constructeur et le destructeur C ++ ne sont pas du tout des fonctions: ce sont des macros. Ils sont alignés dans la portée où l'objet est créé et dans la portée où l'objet est détruit. À son tour, il n'y a ni constructeur ni destructeur, l'objet EST juste.

En fait, je pense que les autres fonctions de la classe ne sont pas des fonctions non plus, mais des fonctions en ligne qui ne sont PAS en ligne parce que vous en prenez l'adresse (le compilateur se rend compte que vous y êtes et n'inline pas ou n'inline pas le code dans la fonction et optimise cette fonction) et à son tour, la fonction semble "toujours là", même si ce ne serait pas le cas si vous ne l'avez pas prise en compte.

La table virtuelle de "l'objet" C ++ n'est pas comme un objet JavaScript, où vous pouvez obtenir son 'constructeur et créer des objets à partir de celui-ci lors de l'exécution via new XMLHttpRequest.constructor, mais plutôt une collection de pointeurs vers des fonctions anonymes qui agissent comme un moyen d'interfacer avec cet objet. , à l'exclusion de la possibilité de créer l'objet. Et cela n'a même pas de sens de "supprimer" l'objet, car c'est comme essayer de supprimer une structure, vous ne pouvez pas: c'est juste une étiquette de pile, écrivez-la comme bon vous semble sous une autre étiquette: vous êtes libre de utilisez une classe comme 4 entiers:

/* 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;

Il n'y a pas de fuite de mémoire, il n'y a pas de problèmes, sauf que vous avez effectivement gaspillé un tas d'espace de pile réservé à l'interface et à la chaîne, mais cela ne détruira pas votre programme (tant que vous n'essayez pas de l'utiliser) comme une chaîne jamais plus).

En fait, si mes hypothèses précédentes sont correctes: le coût complet de la chaîne est juste le coût de stockage de ces 32 octets et de l'espace de chaîne constant: les fonctions ne sont utilisées qu'au moment de la compilation, et peuvent également être intégrées et jetées après l'objet est créé et utilisé (comme si vous travailliez avec une structure et ne vous y référiez que directement sans appel de fonction, assurez-vous qu'il y a des appels en double au lieu des sauts de fonction, mais cela est généralement plus rapide et utilise moins d'espace). En substance, chaque fois que vous appelez une fonction, le compilateur remplace simplement cet appel par les instructions pour le faire littéralement, à des exceptions que les concepteurs de langage ont définies.

Résumé: les objets C ++ n'ont aucune idée de ce qu'ils sont; tous les outils d'interfaçage avec eux sont statiquement intégrés et perdus lors de l'exécution. Cela rend le travail avec les classes aussi efficace que le remplissage des structures avec des données, et le travail direct avec ces données sans appeler aucune fonction (ces fonctions sont intégrées).

Ceci est complètement différent des approches de COM / ObjectiveC ainsi que du javascript, qui conservent les informations de type de manière dynamique, au détriment du temps d'exécution, de la gestion de la mémoire, des appels de constructions, car le compilateur ne peut pas jeter ces informations: c'est nécessaire pour une répartition dynamique. Cela nous donne à son tour la possibilité de «parler» à notre programme au moment de l'exécution et de le développer pendant son exécution en ayant des composants réfléchissants.

Dmitry
la source
2
désolé, mais certaines parties de cette "réponse" sont fausses ou dangereusement trompeuses. Malheureusement, l'espace de commentaires est beaucoup trop petit pour les répertorier tous (la plupart des méthodes ne seront pas intégrées, cela empêcherait la répartition virtuelle et gonflerait le binaire; même s'il est intégré, il pourrait y avoir une copie adressable quelque part accessible; exemple de code non pertinent que dans le pire des cas corrompt votre pile et dans le meilleur des cas ne correspond pas à vos hypothèses; ...)
hoffmale
La réponse est naïve, je voulais juste exprimer ma conjecture sur la raison pour laquelle le constructeur / destructeur ne peut pas être référencé. Je suis d'accord que dans le cas des classes virtuelles, la vtable doit persister et le code adressable doit être en mémoire pour que la vtable puisse le référencer. Cependant, les classes qui n'implémentent pas de classe virtuelle semblent être alignées, comme dans le cas de std :: string. Tout n'est pas aligné, mais des choses qui ne semblent pas être insérées dans un bloc de code "anonyme" quelque part en mémoire. De plus, comment le code endommage-t-il la pile? Bien sûr, nous avons perdu la chaîne, mais sinon, tout ce que nous avons fait est de réinterpréter.
Dmitry
La corruption de la mémoire se produit dans un programme informatique lorsque le contenu d'un emplacement mémoire est involontairement modifié. Ce programme le fait intentionnellement et n'essaie plus d'utiliser cette chaîne, il n'y a donc pas de corruption, juste un espace de pile gaspillé. Mais oui, l'invariant de la chaîne n'est plus maintenu, il encombre la portée (à la fin de quoi, la pile est récupérée).
Dmitry
selon l'implémentation de la chaîne, vous pouvez écraser des octets que vous ne souhaitez pas. Si la chaîne est quelque chose comme struct { int size; const char * data; };(comme vous semblez le supposer), vous écrivez 4 * 4 octets = 16 octets sur une adresse mémoire où vous n'avez réservé que 8 octets sur une machine x86, donc 8 octets sont écrits sur d'autres données (ce qui peut corrompre votre pile ). Heureusement, il std::stringdispose normalement d'une optimisation en place pour les chaînes courtes, il doit donc être suffisamment grand pour votre exemple lors de l'utilisation d'une implémentation std majeure.
hoffmale
@hoffmale vous avez tout à fait raison, cela pourrait être 4 octets, ce pourrait être 8, ou même 1 octet. Cependant, une fois que vous connaissez la taille de la chaîne, vous savez également que cette mémoire est sur la pile dans la portée actuelle, et vous pouvez l'utiliser à votre guise. Mon point était que si vous connaissez la structure, elle est emballée d'une manière indépendante de toute information sur la classe, contrairement aux objets COM qui ont un uuid identifiant leur classe dans le cadre de vtable de IUnknown. À son tour, le compilateur accède à ces données directement via des fonctions statiques intégrées ou modifiées.
Dmitry