Nous devons d'abord revenir à ce que signifie passer par valeur et par référence.
Pour les langages comme Java et SML, le passage par valeur est simple (et il n'y a pas de passage par référence), tout comme la copie d'une valeur de variable est, comme toutes les variables ne sont que des scalaires et ont une sémantique de copie intégrée: ce sont celles qui comptent comme arithmétique tapez en C ++, ou "références" (pointeurs avec un nom et une syntaxe différents).
En C, nous avons des types scalaires et définis par l'utilisateur:
- Les scalaires ont une valeur numérique ou abstraite (les pointeurs ne sont pas des nombres, ils ont une valeur abstraite) qui est copiée.
- Les types d'agrégats ont tous leurs membres éventuellement initialisés copiés:
- pour les types de produits (tableaux et structures): récursivement, tous les membres des structures et éléments des tableaux sont copiés (la syntaxe de la fonction C ne permet pas de passer directement des tableaux par valeur, uniquement les tableaux membres d'une structure, mais c'est un détail ).
- pour les types de somme (unions): la valeur du "membre actif" est préservée; de toute évidence, la copie membre par membre n'est pas en règle car tous les membres ne peuvent pas être initialisés.
En C ++, les types définis par l'utilisateur peuvent avoir une sémantique de copie définie par l'utilisateur, qui permet une programmation véritablement "orientée objet" avec des objets possédant la propriété de leurs ressources et des opérations de "copie profonde". Dans ce cas, une opération de copie est vraiment un appel à une fonction qui peut presque effectuer des opérations arbitraires.
Pour les structures C compilées en C ++, la "copie" est toujours définie comme appelant l'opération de copie définie par l'utilisateur (constructeur ou opérateur d'affectation), qui est implicitement générée par le compilateur. Cela signifie que la sémantique d'un programme de sous-ensemble commun C / C ++ est différente en C et C ++: en C, un type d'agrégat entier est copié, en C ++ une fonction de copie générée implicitement est appelée pour copier chaque membre; le résultat final étant que dans les deux cas, chaque membre est copié.
(Il y a une exception, je pense, quand une structure à l'intérieur d'une union est copiée.)
Donc, pour un type de classe, la seule façon (en dehors des copies d'union) de créer une nouvelle instance est via un constructeur (même pour ceux avec des constructeurs générés par un compilateur trivial).
Vous ne pouvez pas prendre l'adresse d'une valeur r via un opérateur unaire, &
mais cela ne signifie pas qu'il n'y a pas d'objet rvalue; et un objet, par définition, a une adresse ; et cette adresse est même représentée par une construction syntaxique: un objet de type classe ne peut être créé que par un constructeur, et il a un this
pointeur; mais pour les types triviaux, il n'y a pas de constructeur écrit par l'utilisateur donc il n'y a pas de place pour mettrethis
tant que la copie n'est pas construite et nommée.
Pour le type scalaire, la valeur d'un objet est la valeur r de l'objet, la valeur mathématique pure stockée dans l'objet.
Pour un type de classe, la seule notion d'une valeur de l'objet est une autre copie de l'objet, qui ne peut être créée que par un constructeur de copie, une fonction réelle (bien que pour les types triviaux cette fonction soit si spécialement triviale, ceux-ci peuvent parfois être créé sans appeler le constructeur). Cela signifie que la valeur de l'objet est le résultat d'un changement d'état global du programme par une exécution . Il n'y accède pas mathématiquement.
Donc, passer par valeur n'est vraiment pas une chose: c'est passer par l'appel du constructeur de copie , ce qui est moins joli. On s'attend à ce que le constructeur de copie effectue une opération de "copie" sensible selon la sémantique appropriée du type d'objet, en respectant ses invariants internes (qui sont des propriétés utilisateur abstraites, pas des propriétés C ++ intrinsèques).
Passer par la valeur d'un objet de classe signifie:
- créer une autre instance
- puis faites agir la fonction appelée sur cette instance.
Notez que le problème n'a rien à voir avec le fait que la copie elle-même soit un objet avec une adresse: tous les paramètres de fonction sont des objets et ont une adresse (au niveau sémantique du langage).
La question est de savoir si:
- la copie est un nouvel objet initialisé avec la valeur mathématique pure (vraie valeur pure) de l'objet d'origine, comme avec les scalaires;
- ou la copie est la valeur de l'objet d'origine , comme pour les classes.
Dans le cas d'un type de classe trivial, vous pouvez toujours définir le membre du membre copie de l'original, vous pouvez donc définir la valeur r pure de l'original en raison de la trivialité des opérations de copie (constructeur de copie et affectation). Ce n'est pas le cas avec des fonctions utilisateur spéciales arbitraires: une valeur de l'original doit être une copie construite.
Les objets de classe doivent être construits par l'appelant; un constructeur a formellement un this
pointeur mais le formalisme n'est pas pertinent ici: tous les objets ont formellement une adresse mais seuls ceux qui obtiennent leur adresse utilisée de manière non purement locale (contrairement à *&i = 1;
ce qui est une utilisation purement locale de l'adresse) doivent avoir une définition bien définie adresse.
Un objet doit absolument être passé par adresse s'il doit sembler avoir une adresse dans ces deux fonctions compilées séparément:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
Ici, même s'il something(address)
s'agit d'une fonction ou d'une macro pure ou autre (comme printf("%p",arg)
) qui ne peut pas stocker l'adresse ou communiquer avec une autre entité, nous avons l'obligation de passer par adresse car l'adresse doit être bien définie pour un objet uniqueint
qui a un unique identité.
Nous ne savons pas si une fonction externe sera "pure" en termes d'adresses qui lui seront transmises.
Ici, le potentiel d'une utilisation réelle de l'adresse dans un constructeur ou un destructeur non trivial du côté de l'appelant est probablement la raison de prendre la route simpliste et sécurisée et de donner à l'objet une identité dans l'appelant et de transmettre son adresse, comme il le fait sûr que toute utilisation non triviale de son adresse dans le constructeur, après la construction et dans le destructeur est cohérente : this
doit sembler être la même sur l'existence de l'objet.
Un constructeur ou destructeur non trivial comme toute autre fonction peut utiliser le this
pointeur d'une manière qui requiert une cohérence sur sa valeur même si certains objets avec des éléments non triviaux ne le peuvent pas:
struct file_handler { // don't use that class!
file_handler () { this->fileno = -1; }
file_handler (int f) { this->fileno = f; }
file_handler (const file_handler& rhs) {
if (this->fileno != -1)
this->fileno = dup(rhs.fileno);
else
this->fileno = -1;
}
~file_handler () {
if (this->fileno != -1)
close(this->fileno);
}
file_handler &operator= (const file_handler& rhs);
};
Notez que dans ce cas, malgré l'utilisation explicite d'un pointeur (syntaxe explicite this->
), l'identité de l'objet n'est pas pertinente: le compilateur pourrait bien utiliser la copie au niveau du bit de l'objet pour le déplacer et pour faire une "élision de copie". Ceci est basé sur le niveau de "pureté" de l'utilisation de this
dans les fonctions membres spéciales (l'adresse n'échappe pas).
Mais la pureté n'est pas un attribut disponible au niveau de la déclaration standard (il existe des extensions de compilateur qui ajoutent une description de la pureté sur la déclaration de fonction non en ligne), vous ne pouvez donc pas définir un ABI basé sur la pureté du code qui peut ne pas être disponible (le code peut ou peuvent ne pas être en ligne et disponibles pour analyse).
La pureté est mesurée comme «certainement pure» ou «impure ou inconnue». Le terrain d'entente, ou limite supérieure de la sémantique (en fait maximum), ou LCM (Least Common Multiple) est "inconnu". Ainsi, l'ABI s'installe sur inconnu.
Sommaire:
- Certaines constructions nécessitent que le compilateur définisse l'identité de l'objet.
- L'ABI est défini en termes de classes de programmes et non de cas spécifiques qui pourraient être optimisés.
Travaux futurs possibles:
L'annotation de pureté est-elle suffisamment utile pour être généralisée et standardisée?