Pourquoi un T * peut-il être passé dans le registre, mais un unique_ptr <T> ne peut pas?

85

Je regarde le discours de Chandler Carruth dans CppCon 2019:

Il n'y a pas d'abstractions à coût nul

dans ce document, il donne l'exemple de la façon dont il a été surpris par la quantité de frais généraux que vous encourez en utilisant un std::unique_ptr<int>sur un int*; ce segment commence au point de temps 17:25.

Vous pouvez consulter les résultats de la compilation de son exemple de paire d'extraits de code (godbolt.org) - pour constater qu'en effet, il semble que le compilateur ne souhaite pas transmettre la valeur unique_ptr - qui, en fait, en fin de compte, est juste une adresse - à l'intérieur d'un registre, uniquement en mémoire pure.

L'un des points soulevés par M. Carruth vers 27h00 est que l'ABI C ++ nécessite que des paramètres de sous-valeur (certains mais pas tous; peut-être - des types non primitifs? Des types non trivialement constructibles?) Soient passés en mémoire plutôt que dans un registre.

Mes questions:

  1. Est-ce réellement une exigence ABI sur certaines plateformes? (lequel?) Ou peut-être que c'est juste une pessimisation dans certains scénarios?
  2. Pourquoi l'ABI est-il comme ça? Autrement dit, si les champs d'une structure / classe s'inscrivent dans des registres, ou même dans un seul registre - pourquoi ne pourrions-nous pas le transmettre dans ce registre?
  3. Le comité des normes C ++ a-t-il discuté de ce point ces dernières années, ou jamais?

PS - Pour ne pas laisser cette question sans code:

Pointeur simple:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Pointeur unique:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}
einpoklum
la source
8
Je ne suis pas sûr de ce que l'exigence ABI est exactement, mais cela n'interdit pas de mettre des structures dans les registres
harold
6
Si je devais deviner, je dirais que cela a à voir avec les fonctions membres non triviales nécessitant un thispointeur qui pointe vers un emplacement valide. unique_ptra ceux-là. Renverser le registre à cet effet annulerait en quelque sorte toute l'optimisation "passer dans un registre".
Conteur - Unslander Monica
2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls . Donc, ce comportement était nécessaire. Pourquoi? itanium-cxx-abi.github.io/cxx-abi/cxx-closed.html , recherchez le problème C-7. Il y a là une explication, mais elle n'est pas trop détaillée. Mais oui, ce comportement ne me semble pas logique. Ces objets peuvent être passés à travers la pile normalement. Les pousser à s'empiler, puis passer la référence (juste pour les objets "non triviaux") semble un gaspillage.
geza
6
Il semble que C ++ viole ses propres principes ici, ce qui est assez triste. J'étais convaincu à 140% que tout unique_ptr disparaît juste après la compilation. Après tout, ce n'est qu'un appel de destructeur différé qui est connu au moment de la compilation.
One Man Monkey Squad
7
@MaximEgorushkin: Si vous l'aviez écrit à la main, vous auriez placé le pointeur dans un registre et non sur la pile.
einpoklum

Réponses:

49
  1. Est-ce en fait une exigence ABI, ou peut-être que c'est juste une pessimisation dans certains scénarios?

Un exemple est System V Application Binary Interface Architecture AMD64 Supplément processeur . Cette ABI est destinée aux CPU 64 bits compatibles x86 (architecture Linux x86_64). Il est suivi sur Solaris, Linux, FreeBSD, macOS, sous-système Windows pour Linux:

Si un objet C ++ possède un constructeur de copie non trivial ou un destructeur non trivial, il est transmis par référence invisible (l'objet est remplacé dans la liste des paramètres par un pointeur de classe INTEGER).

Un objet avec un constructeur de copie non trivial ou un destructeur non trivial ne peut pas être transmis par valeur car ces objets doivent avoir des adresses bien définies. Des problèmes similaires s'appliquent lors du retour d'un objet à partir d'une fonction.

Notez que seuls 2 registres à usage général peuvent être utilisés pour passer 1 objet avec un constructeur de copie trivial et un destructeur trivial, c'est-à-dire que seules les valeurs des objets sizeofne dépassant pas 16 peuvent être passées dans les registres. Voir Conventions d'appel par Agner Fog pour un traitement détaillé des conventions d'appel, en particulier §7.1 Passage et retour d'objets. Il existe des conventions d'appel distinctes pour le passage des types SIMD dans les registres.

Il existe différentes ABI pour d'autres architectures de CPU.


  1. Pourquoi l'ABI est-il comme ça? Autrement dit, si les champs d'une structure / classe s'inscrivent dans des registres, ou même dans un seul registre - pourquoi ne pourrions-nous pas le transmettre dans ce registre?

Il s'agit d'un détail d'implémentation, mais lorsqu'une exception est gérée, lors du déroulement de la pile, les objets dont la durée de stockage automatique est détruite doivent être adressables par rapport au cadre de la pile de fonctions car les registres ont été encombrés à ce moment-là. Le code de déroulement de pile a besoin des adresses des objets pour invoquer leurs destructeurs mais les objets dans les registres n'ont pas d'adresse.

Pédantiquement, les destructeurs opèrent sur des objets :

Un objet occupe une région de stockage dans sa période de construction ([class.cdtor]), tout au long de sa durée de vie et dans sa période de destruction.

et un objet ne peut pas exister en C ++ si aucun stockage adressable ne lui est alloué car l'identité de l'objet est son adresse .

Lorsqu'une adresse d'un objet avec un constructeur de copie trivial conservé dans des registres est nécessaire, le compilateur peut simplement stocker l'objet en mémoire et obtenir l'adresse. Si le constructeur de copie n'est pas trivial, en revanche, le compilateur ne peut pas simplement le stocker en mémoire, il doit plutôt appeler le constructeur de copie qui prend une référence et nécessite donc l'adresse de l'objet dans les registres. La convention d'appel ne peut probablement pas dépendre du fait que le constructeur de copie a été inséré dans l'appelé ou non.

Une autre façon de penser à cela est que pour les types trivialement copiables, le compilateur transfère la valeur d'un objet dans des registres, à partir desquels un objet peut être récupéré par des mémoires de mémoire ordinaire si nécessaire. Par exemple:

void f(long*);
void g(long a) { f(&a); }

sur x86_64 avec System V ABI se compile en:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

Dans son discours incitant à la réflexion, Chandler Carruth mentionne qu'un changement d'ABI cassant peut être nécessaire (entre autres) pour mettre en œuvre le mouvement destructeur qui pourrait améliorer les choses. IMO, le changement ABI pourrait être sans rupture si les fonctions utilisant le nouvel ABI optent explicitement pour avoir une nouvelle liaison différente, par exemple les déclarer en extern "C++20" {}bloc (éventuellement, dans un nouvel espace de noms en ligne pour la migration des API existantes). Pour que seul le code compilé avec les nouvelles déclarations de fonction avec la nouvelle liaison puisse utiliser le nouvel ABI.

Notez que l'ABI ne s'applique pas lorsque la fonction appelée a été insérée. Comme pour la génération de code au moment de la liaison, le compilateur peut incorporer des fonctions définies dans d'autres unités de traduction ou utiliser des conventions d'appel personnalisées.

Maxim Egorushkin
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew
8

Avec les ABI courants, le destructeur non trivial -> ne peut pas passer dans les registres

(Une illustration d'un point dans la réponse de @ MaximEgorushkin utilisant l'exemple de @ harold dans un commentaire; corrigé selon le commentaire de @ Yakk.)

Si vous compilez:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

vous obtenez:

test(Foo):
        mov     eax, edi
        ret

c'est-à-dire que l' Fooobjet est passé àtest dans un registre ( edi) et également retourné dans un registre ( eax).

Lorsque le destructeur n'est pas trivial (comme le std::unique_ptr exemple des OP) - Les ABI communs nécessitent un placement sur la pile. Cela est vrai même si le destructeur n'utilise pas du tout l'adresse de l'objet.

Ainsi, même dans le cas extrême d'un destructeur ne rien faire, si vous compilez:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

vous obtenez:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

avec chargement et stockage inutiles.

einpoklum
la source
Je ne suis pas convaincu par cet argument. Le destructeur non trivial ne fait rien pour interdire la règle comme si. Si l'adresse n'est pas respectée, il n'y a absolument aucune raison pour qu'il y en ait une. Ainsi, un compilateur conforme pourrait heureusement le mettre dans un registre, si cela ne change pas le comportement observable (et les compilateurs actuels le feront en fait si les appelants sont connus ).
ComicSansMS du
1
Malheureusement, c'est l'inverse (je suis d'accord pour dire que cela dépasse déjà la raison). Pour être précis: je ne suis pas convaincu que les raisons que vous avez fournies rendraient nécessairement tout ABI imaginable permettant de faire passer le courant std::unique_ptrdans un registre non conforme.
ComicSansMS
3
«destructeur trivial [CITATION NÉCESSAIRE]» clairement faux; si aucun code ne dépend réellement de l'adresse, alors comme si cela signifie que l'adresse n'a pas besoin d'exister sur la machine réelle . L'adresse doit exister dans la machine abstraite , mais les choses dans la machine abstraite qui n'ont aucun impact sur la machine réelle sont des choses comme si elles pouvaient être éliminées.
Yakk - Adam Nevraumont
2
@einpoklum Rien dans la norme n'existe de registres d'états. Le mot-clé d'enregistrement indique simplement "vous ne pouvez pas prendre l'adresse". Il n'y a qu'une machine abstraite en ce qui concerne la norme. "comme si" signifie que toute implémentation de machine réelle n'a besoin que de se comporter "comme si" la machine abstraite se comporte, jusqu'à un comportement non défini par la norme. Maintenant, il y a des problèmes très difficiles à avoir un objet dans un registre, dont tout le monde a beaucoup parlé. En outre, les conventions d'appel, dont la norme ne traite pas non plus, ont des besoins pratiques.
Yakk - Adam Nevraumont du
1
@einpoklum Non, dans cette machine abstraite, toutes choses ont des adresses; mais les adresses ne sont observables que dans certaines circonstances. Le registermot-clé était destiné à rendre trivial pour la machine physique de stocker quelque chose dans un registre en bloquant les choses qui rendent pratiquement plus difficile de "ne pas avoir d'adresse" dans la machine physique.
Yakk - Adam Nevraumont
2

Est-ce réellement une exigence ABI sur certaines plateformes? (lequel?) Ou peut-être que c'est juste une pessimisation dans certains scénarios?

Si quelque chose est visible à la frontière de l'unité de complication, qu'il soit défini implicitement ou explicitement, il fait partie de l'ABI.

Pourquoi l'ABI est-il comme ça?

Le problème fondamental est que les registres sont enregistrés et restaurés tout le temps lorsque vous montez et descendez dans la pile des appels. Il n'est donc pas pratique d'avoir une référence ou un pointeur vers eux.

L'intégration et les optimisations qui en découlent sont agréables quand cela se produit, mais un concepteur ABI ne peut pas compter sur cela. Ils doivent concevoir l'ABI en supposant le pire des cas. Je ne pense pas que les programmeurs seraient très satisfaits d'un compilateur où l'ABI a changé en fonction du niveau d'optimisation.

Un type trivialement copiable peut être passé dans les registres car l'opération de copie logique peut être divisée en deux parties. Les paramètres sont copiés dans les registres utilisés pour transmettre les paramètres par l'appelant, puis copiés dans la variable locale par l'appelé. Le fait que la variable locale ait ou non un emplacement mémoire n'est donc que la préoccupation de l'appelé.

Un type où un constructeur de copie ou de déplacement doit être utilisé d'autre part ne peut pas avoir son opération de copie divisée de cette manière, il doit donc être passé en mémoire.

Le comité des normes C ++ a-t-il discuté de ce point ces dernières années, ou jamais?

Je ne sais pas si les organismes de normalisation ont envisagé cela.

La solution évidente pour moi serait d'ajouter des mouvements destructeurs appropriés (plutôt que la maison à mi-chemin actuelle d'un "état valide mais non spécifié") à la jauge, puis d'introduire un moyen de signaler un type comme permettant des "mouvements destructeurs triviaux "même s'il ne permet pas de copies triviales.

mais une telle solution DEVRAIT nécessiter la rupture de l'ABI du code existant à implémenter pour les types existants, ce qui peut apporter un peu de résistance (bien que les ruptures ABI à la suite de nouvelles versions standard C ++ ne soient pas sans précédent, par exemple les changements std :: string en C ++ 11 a entraîné une rupture ABI ..

plugwash
la source
Pouvez-vous expliquer comment des mouvements destructeurs appropriés permettraient de passer un unique_ptr dans un registre? Serait-ce parce que cela permettrait de supprimer l'exigence de stockage adressable?
einpoklum
Des mouvements destructeurs appropriés permettraient d'introduire un concept de mouvements destructeurs triviaux. Cela permettrait à ce mouvement trivial d'être divisé par l'ABI de la même manière que les copies triviales peuvent l'être aujourd'hui.
plugwash
Bien que vous souhaitiez également ajouter une règle selon laquelle un compilateur pourrait implémenter un passage de paramètre comme un mouvement ou une copie régulière suivi d'un "mouvement destructeur trivial" pour garantir qu'il était toujours possible de passer dans les registres, peu importe d'où venait le paramètre.
plugwash
Parce que la taille du registre peut contenir un pointeur, mais une structure unique_ptr? Quelle est la taille de (unique_ptr <T>)?
Mel Viso Martinez
@MelVisoMartinez Vous pouvez être déroutant unique_ptret shared_ptrsémantique: shared_ptr<T>vous permet de fournir au ctor 1) un ptr x à l'objet dérivé U à supprimer avec le type statique U w / l'expression delete x;(vous n'avez donc pas besoin d'un dtor virtuel ici) 2) ou même une fonction de nettoyage personnalisée. Cela signifie que l'état d'exécution est utilisé à l'intérieur du shared_ptrbloc de contrôle pour coder ces informations. OTOH unique_ptrn'a pas une telle fonctionnalité et n'encode pas le comportement de suppression dans l'état; la seule façon de personnaliser le nettoyage est de créer une autre instanciation de modèle (un autre type de classe).
curiousguy
-1

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 thispointeur; 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 thispointeur 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 : thisdoit sembler être la même sur l'existence de l'objet.

Un constructeur ou destructeur non trivial comme toute autre fonction peut utiliser le thispointeur 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 thisdans 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?

curiousguy
la source
1
Votre premier exemple semble trompeur. Je pense que vous faites simplement une remarque générale, mais au début, je pensais que vous faisiez une analogie avec le code de la question. Mais void foo(unique_ptr<int> ptr)prend l'objet de classe par valeur . Cet objet a un membre pointeur, mais nous parlons de l'objet de classe lui-même transmis par référence. (Parce qu'il n'est pas trivialement copiable, son constructeur / destructeur a donc besoin d'un cohérent this.) C'est le vrai argument et non connecté au premier exemple de passage par référence explicitement ; dans ce cas, le pointeur est passé dans un registre.
Peter Cordes
@PeterCordes " vous faisiez une analogie avec le code de la question. " C'est exactement ce que j'ai fait. " l'objet de classe par valeur " Oui, je devrais probablement expliquer qu'en général, la "valeur" d'un objet de classe n'existe pas, donc par valeur pour un type non mathématique ce n'est pas "par valeur". " Cet objet a un membre de pointeur " La nature de type ptr d'un "ptr intelligent" n'est pas pertinente; il en est de même du membre ptr du "smart ptr". Un ptr est juste un scalaire comme un int: j'ai écrit un exemple de "smart fileno" qui illustre que la "propriété" n'a rien à voir avec "porter un ptr".
curiousguy
1
La valeur d'un objet de classe est sa représentation d'objet. Pour unique_ptr<T*>, il s'agit de la même taille et de la même disposition que T*dans un registre. Les objets de classe copiables de manière triviale peuvent être passés par valeur dans les registres du système V x86-64, comme la plupart des conventions d'appel. Cela fait une copie de l' unique_ptrobjet, contrairement à votre intexemple où l'appelé &i est l'adresse de l'appelant icar vous avez passé par référence au niveau C ++ , pas seulement comme détail d'implémentation asm.
Peter Cordes
1
Euh, correction de mon dernier commentaire. Il ne s'agit pas seulement de faire une copie de l' unique_ptrobjet; il utilise std::movedonc il est sûr de le copier car cela n'entraînera pas 2 copies du même unique_ptr. Mais pour un type trivialement copiable, oui, il copie tout l'objet agrégé. S'il s'agit d'un seul membre, les bonnes conventions d'appel le traitent de la même manière qu'un scalaire de ce type.
Peter Cordes
1
Regarde mieux. Remarques: Pour les structures C compilées en C ++ - Ce n'est pas un moyen utile pour introduire la différence entre C ++. En C ++ struct{}est une structure C ++. Vous devriez peut-être dire «structures simples» ou «contrairement à C». Parce que oui, il y a une différence. Si vous utilisez atomic_intcomme membre struct, C le copiera de manière non atomique, erreur C ++ sur le constructeur de copie supprimé. J'oublie ce que fait C ++ sur les structures avec les volatilemembres. C vous permettra struct tmp = volatile_struct;de copier le tout (utile pour un SeqLock); C ++ ne le fera pas.
Peter Cordes