38 votes

Pourquoi les paramètres par valeur sont-ils exclus du NRVO ?

Imaginez :

S f(S a) {
  return a;
}

Pourquoi n'est-il pas permis d'aliaser a et la fente de la valeur de retour ?

S s = f(t);
S s = t; // can't generally transform it to this :(

La spécification ne permet pas cette transformation si le constructeur de copie de S a des effets secondaires. Au lieu de cela, il nécessite au moins deux copies (une de t a a et un autre de a à la valeur de retour, et une autre de la valeur de retour à s et seule cette dernière peut être élidée. Notez que j'ai écrit = t ci-dessus pour représenter le fait d'une copie de t à f's a (la seule copie qui serait encore obligatoire en présence d'effets secondaires du constructeur move/copy).

Pourquoi ça ?

20voto

Nicol Bolas Points 133791

Voici pourquoi l'élision de copie n'a pas de sens pour les paramètres. Il s'agit en fait de l'implémentation du concept au niveau du compilateur.

L'élision de copie fonctionne essentiellement en construisant la valeur de retour sur place. La valeur n'est pas copiée, elle est créée directement dans sa destination prévue. C'est l'appelant qui fournit l'espace pour la sortie prévue, et donc c'est finalement l'appelant qui fournit la possibilité de l'élision.

Tout ce que la fonction doit faire en interne pour élider la copie est de construire la sortie à l'endroit fourni par l'appelant. Si la fonction peut le faire, vous obtenez l'élision de la copie. Si la fonction ne peut pas le faire, elle utilisera une ou plusieurs variables temporaires pour stocker les résultats intermédiaires, puis les copier/déplacer à l'endroit fourni par l'appelant. Le résultat est toujours construit in-place, mais la construction de la sortie se fait par copie.

Ainsi, le monde extérieur à une fonction particulière n'a pas à savoir ou à se soucier de savoir si une fonction fait l'élision. Plus précisément, le appelant de la fonction n'a pas besoin de savoir comment la fonction est mise en œuvre. Il ne fait rien de différent ; c'est la fonction elle-même qui décide si l'élision est possible.

Le stockage des paramètres de valeur est également fourni par l'appelant. Lorsque vous appelez f(t) c'est l'appelant qui crée la copie du fichier t et le transmet à f . De même, si S est implicitement constructible à partir d'un int entonces f(5) construira un S du 5 et le passe à f .

Tout cela est fait par le appelant . L'appelé ne sait pas ou ne se soucie pas de savoir s'il s'agit d'une variable ou d'un temporaire ; on lui donne simplement un emplacement de la mémoire de la pile (ou des registres ou autre).

Rappelez-vous : l'élision par copie fonctionne parce que la fonction appelée construit la variable directement dans l'emplacement de sortie. Donc si vous essayez d'élider le retour d'un paramètre de valeur, alors le stockage du paramètre de valeur doit également être le stockage de sortie lui-même . Mais n'oubliez pas : c'est le appelant qui fournit ce stockage à la fois pour le paramètre et la sortie. Et donc, pour éluder la copie de sortie, l'élément appelant doit construire le paramètre directement dans le sortie .

Pour ce faire, l'appelant doit maintenant savoir que la fonction qu'il appelle élidera la valeur de retour, car il ne peut coller le paramètre directement dans la sortie que si le paramètre est retourné. Cela ne sera généralement pas possible au niveau du compilateur, car l'appelant ne dispose pas nécessairement de l'implémentation de la fonction. Si la fonction est inlined, alors peut-être que cela peut fonctionner. Mais sinon, non.

Par conséquent, le comité C++ n'a pas pris la peine de tenir compte de cette possibilité.

3voto

La raison de cette restriction, telle que je la comprends, est que la convention d'appel peut exiger (et exigera dans de nombreux cas) que l'argument de la fonction et l'objet de retour se trouvent à des emplacements différents (soit en mémoire, soit dans des registres). Considérons l'exemple modifié suivant :

X foo();
X bar( X a ) 
{ 
   return a;
}
int main() {
   X x = bar( foo() );
}

En théorie, l'ensemble des copies serait une déclaration de retour en foo ( $tmp1 ), l'argument a de bar , déclaration de retour de bar ( $tmp2 ) et x en main . Les compilateurs peuvent élider deux des quatre objets en créant des $tmp1 à l'emplacement de a y $tmp2 à l'emplacement de x . Lorsque le compilateur traite main on peut noter que la valeur de retour de foo est l'argument de bar et peut les faire coïncider, à ce moment-là il ne peut pas savoir (sans inlining) que l'argument et le retour de bar sont le même objet, et il doit se conformer à la convention d'appel, donc il placera $tmp1 dans la position de l'argument à bar .

En même temps, il sait que le but de l'action de l $tmp2 ne fait que créer x Il peut donc placer les deux à la même adresse. A l'intérieur de bar il n'y a pas grand chose à faire : l'argument a est situé à la place du premier argument, selon la convention d'appel, et $tmp2 doit être localisé selon la convention d'appel, (dans le cas général dans un endroit différent, pensez que l'exemple peut être étendu à une bar qui prend plusieurs arguments, dont un seul est utilisé comme déclaration de retour.

Maintenant, si le compilateur effectue l'inlining, il pourrait détecter que la copie supplémentaire qui serait nécessaire si la fonction n'était pas inlined n'est pas vraiment nécessaire, et il aurait une chance de l'élider. Si la norme permettait d'élider cette copie particulière, le même code aurait des comportements différents selon que la fonction est inlined ou non.

1voto

Öö Tiib Points 4755

De t à a, il est déraisonnable d'élider la copie. Le paramètre est déclaré mutable, donc la copie est faite parce qu'on s'attend à ce qu'il soit modifié dans la fonction.

De l'a à la valeur de retour, je ne vois aucune raison de copier. Peut-être s'agit-il d'une sorte d'oubli ? Les paramètres par valeur sont comme des locaux dans le corps de la fonction ... je ne vois pas de différence.

1voto

Clinton Points 5556

David Rodríguez - dribeas réponse à ma question "Comment permettre la copie élision de la construction pour les classes C++' m'a donné l'idée suivante. L'astuce consiste à utiliser des lambdas de retarder l'évaluation jusqu'à l'intérieur du corps de la fonction:

#include <iostream>

struct S
{
  S() {}
  S(const S&) { std::cout << "Copy" << std::endl; }
  S(S&&) { std::cout << "Move" << std::endl; }
};

S f1(S a) {
  return a;
}

S f2(const S& a) {
  return a;
}

#define DELAY(x) [&]{ return x; }

template <class F>
S f3(const F& a) {
  return a();
}

int main()
{
  S t;
  std::cout << "Without delay:" << std::endl;
  S s1 = f1(t);
  std::cout << "With delay:" << std::endl;
  S s2 = f3(DELAY(t));
  std::cout << "Without delay pass by ref:" << std::endl;
  S s3 = f2(t);
  std::cout << "Without delay pass by ref (temporary) (should have 0 copies, will get 1):" << std::endl;
  S s4 = f2(S());
  std::cout << "With delay (temporary) (no copies, best):" << std::endl;
  S s5 = f3(DELAY(S()));
}

Ce sorties sur ideone GCC 4.5.1:

Sans délai:
Copie
Copie
Avec retard:
Copie

Maintenant, c'est bon, mais on pourrait suggérer que le RETARD de la version est juste comme le passage par référence const, comme ci-dessous:

Sans tarder passer par réf.:
Copie

Mais si on passe temporaire par const référence, nous obtenons toujours une copie:

Sans tarder passer par ref (temporaire) (doit être 0 exemplaires, recevez 1):
Copie

D'où le retard de la version élude la copie:

Avec retard (temporaire) (pas de copies, le meilleur):

Comme vous pouvez le voir, cette élude toutes les copies temporaires cas.

Le retard de la version du produit une copie de la non-temporaire cas, et aucune copie dans le cas d'une mesure temporaire. Je ne sais pas de toute façon d'atteindre cet objectif autre que les lambdas, mais je serais intéressé si il y est.

-1voto

bdow Points 155

Je pense que le problème est que si le constructeur de copie fait quelque chose, alors le compilateur doit faire cette chose un nombre prévisible de fois. Si vous avez une classe qui incrémente un compteur à chaque fois qu'elle est copiée, par exemple, et qu'il existe un moyen d'accéder à ce compteur, alors un compilateur conforme aux normes doit effectuer cette opération un nombre de fois bien défini (sinon, comment écrirait-on des tests unitaires ?).

Maintenant, c'est probablement une mauvaise idée d'écrire une classe comme ça, mais ce n'est pas le travail du compilateur de le découvrir, seulement de s'assurer que la sortie est correcte et cohérente.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X