Comment «retourner un objet» en C ++?

167

Je sais que le titre semble familier car il y a beaucoup de questions similaires, mais je demande un aspect différent du problème (je connais la différence entre avoir des choses sur la pile et les mettre sur le tas).

En Java, je peux toujours renvoyer des références à des objets "locaux"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

En C ++, pour faire quelque chose de similaire, j'ai 2 options

(1) Je peux utiliser des références chaque fois que j'ai besoin de "retourner" un objet

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Alors utilisez-le comme ça

Thing thing;
calculateThing(thing);

(2) Ou je peux renvoyer un pointeur vers un objet alloué dynamiquement

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Alors utilisez-le comme ça

Thing* thing = calculateThing();
delete thing;

En utilisant la première approche, je n'aurai pas à libérer de la mémoire manuellement, mais pour moi, cela rend le code difficile à lire. Le problème avec la deuxième approche est, je dois m'en souvenir delete thing;, qui n'a pas l'air très bien. Je ne veux pas renvoyer une valeur copiée car elle est inefficace (je pense), alors voici les questions

  • Existe-t-il une troisième solution (qui ne nécessite pas de copier la valeur)?
  • Y a-t-il un problème si je m'en tiens à la première solution?
  • Quand et pourquoi devrais-je utiliser la deuxième solution?
Phunehehe
la source
32
+1 pour bien poser la question.
Kangkan
1
Pour être très pédant, il est un peu imprécis de dire que «les fonctions renvoient quelque chose». Plus correctement, l' évaluation d'un appel de fonction produit une valeur . La valeur est toujours un objet (sauf s'il s'agit d'une fonction void). La distinction est de savoir si la valeur est une glvalue ou une prvalue - qui est déterminée par si le type de retour déclaré est une référence ou non.
Kerrek SB

Réponses:

107

Je ne veux pas renvoyer une valeur copiée car elle est inefficace

Prouve le.

Recherchez RVO et NRVO, et dans la sémantique de déplacement C ++ 0x. Dans la plupart des cas en C ++ 03, un paramètre out est juste un bon moyen de rendre votre code moche, et en C ++ 0x, vous vous feriez mal en utilisant un paramètre out.

Écrivez simplement du code propre, retournez par valeur. Si les performances sont un problème, profilez-le (arrêtez de deviner) et trouvez ce que vous pouvez faire pour y remédier. Cela ne retournera probablement pas les choses des fonctions.


Cela dit, si vous êtes déterminé à écrire comme ça, vous voudrez probablement utiliser le paramètre out. Il évite l'allocation dynamique de mémoire, qui est plus sûre et généralement plus rapide. Cela nécessite que vous ayez un moyen de construire l'objet avant d'appeler la fonction, ce qui n'a pas toujours de sens pour tous les objets.

Si vous souhaitez utiliser l'allocation dynamique, le moins que l'on puisse faire est de la placer dans un pointeur intelligent. (Cela devrait être fait tout le temps de toute façon) Ensuite, vous ne vous inquiétez pas de supprimer quoi que ce soit, les choses sont protégées contre les exceptions, etc. Le seul problème est que c'est probablement plus lent que de retourner par valeur de toute façon!

GManNickG
la source
10
@phunehehe: Aucun point ne spécule, vous devez profiler votre code et le découvrir. (Indice: non.) Les compilateurs sont très intelligents, ils ne perdront pas de temps à copier des choses s'ils ne sont pas obligés de le faire. Même si la copie coûte quelque chose, vous devez toujours vous efforcer d'obtenir un bon code plutôt qu'un code rapide; un bon code est facile à optimiser lorsque la vitesse devient un problème. Inutile de déranger du code pour quelque chose dont vous n'avez aucune idée, c'est un problème; surtout si vous le ralentissez ou n'en retirez rien. Et si vous utilisez C ++ 0x, la sémantique de déplacement en fait un non-problème.
GManNickG
1
@GMan, re: RVO: en fait, cela n'est vrai que si votre appelant et votre appelé se trouvent dans la même unité de compilation, ce qui dans le monde réel ce n'est pas la plupart du temps. Donc, vous serez déçu si votre code n'est pas entièrement basé sur un modèle (auquel cas il sera dans une seule unité de compilation) ou si vous avez une optimisation du temps de liaison en place (GCC ne l'a que depuis 4.5).
Alex B
2
@Alex: Les compilateurs s'améliorent de plus en plus dans l'optimisation des unités de traduction. (VC le fait pour plusieurs versions maintenant.)
sbi
9
@Alex B: C'est une poubelle complète. De nombreuses conventions d'appel très courantes rendent l'appelant responsable de l'allocation d'espace pour les valeurs de retour importantes et l'appelé responsable de leur construction. RVO fonctionne avec plaisir sur les unités de compilation, même sans optimisation du temps de liaison.
CB Bailey
6
@Charles, après vérification, cela semble correct! Je retire ma déclaration clairement mal informée.
Alex B
41

Créez simplement l'objet et renvoyez-le

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Je pense que vous vous ferez une faveur si vous oubliez l'optimisation et écrivez simplement du code lisible (vous devrez exécuter un profileur plus tard - mais ne pré-optimisez pas).

Amir Rachum
la source
2
Thing thing();déclare une fonction locale et renvoie un fichier Thing.
dreamlax
2
Thing thing () déclare une fonction retournant une chose. Il n'y a pas d'objet Thing construit dans votre corps de fonction.
CB Bailey
@dreamlax @Charles @GMan Un peu tard, mais corrigé.
Amir Rachum
Comment cela fonctionne-t-il en C ++ 98? J'obtiens des erreurs sur l'interpréteur CINT et je me demandais que c'était dû à C ++ 98 ou CINT lui-même ...!
xcorat
16

Renvoyez simplement un objet comme celui-ci:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Cela appellera le constructeur de copie sur Things, donc vous voudrez peut-être en faire votre propre implémentation. Comme ça:

Thing(const Thing& aThing) {}

Cela peut fonctionner un peu plus lentement, mais ce n'est peut-être pas du tout un problème.

Mettre à jour

Le compilateur optimisera probablement l'appel au constructeur de copie, donc il n'y aura pas de surcharge supplémentaire. (Comme dreamlax souligné dans le commentaire).

Martin Ingvar Kofoed Jensen
la source
9
Thing thing();déclare une fonction locale renvoyant un Thing, également, le standard permet au compilateur d'omettre le constructeur de copie dans le cas que vous avez présenté; n'importe quel compilateur moderne le fera probablement.
dreamlax
1
Vous apportez un bon point en implémentant le constructeur de copie, surtout si une copie profonde est nécessaire.
mbadawi23
+1 pour indiquer explicitement le constructeur de copie, bien que comme @dreamlax le dit, le compilateur "optimisera" très probablement le code de retour pour les fonctions en évitant un appel pas vraiment nécessaire au constructeur de copie.
jose.angel.jimenez
En 2018, dans VS 2017, il tente d'utiliser le constructeur de mouvement. Si le constructeur de déplacement est supprimé et que le constructeur de copie ne l'est pas, il ne sera pas compilé.
Andrew
11

Avez-vous essayé d'utiliser des pointeurs intelligents (si Thing est un objet vraiment gros et lourd), comme auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}
demetrios
la source
4
auto_ptrs sont obsolètes; utilisez shared_ptrou à la unique_ptrplace.
MBraedley
Je vais juste ajouter ceci ici ... J'utilise C ++ depuis des années, mais pas professionnellement avec C ++ ... J'ai décidé d'essayer de ne plus utiliser de pointeurs intelligents, ils sont juste un désordre absolu imo et causent tout sortes de problèmes, n'aidant pas vraiment à accélérer le code non plus. Je préfère tellement simplement copier les données et gérer moi-même les pointeurs, en utilisant RAII. Je vous conseille donc, si vous le pouvez, d'éviter les pointeurs intelligents.
Andrew
8

Un moyen rapide de déterminer si un constructeur de copie est appelé est d'ajouter la journalisation au constructeur de copie de votre classe:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Appel someFunction; le nombre de lignes "Copy constructor was called" que vous obtiendrez variera entre 0, 1 et 2. Si vous n'en obtenez aucune, alors votre compilateur a optimisé la valeur de retour (ce qu'il est autorisé à faire). Si vous ne prenez pas 0, et votre constructeur de copie est ridiculement cher, puis chercher d' autres moyens pour retourner les instances de vos fonctions.

dreamlax
la source
1

Premièrement, vous avez une erreur dans le code, vous voulez avoir Thing *thing(new Thing());, et seulement return thing;.

  • Utilisez shared_ptr<Thing>. Deref comme si c'était un pointeur. Il sera supprimé pour vous lorsque la dernière référence au Thingcontenu sortira du champ d'application.
  • La première solution est très courante dans les bibliothèques naïves. Il a des performances et une surcharge syntaxique, évitez-le si possible
  • Utilisez la deuxième solution uniquement si vous pouvez garantir qu'aucune exception ne sera levée, ou lorsque les performances sont absolument critiques (vous interagirez avec C ou l'assembly avant que cela ne devienne pertinent).
Matt Joiner
la source
0

Je suis sûr qu'un expert C ++ viendra avec une meilleure réponse, mais personnellement j'aime la deuxième approche. L'utilisation de pointeurs intelligents aide à résoudre le problème de l'oubli deleteet, comme vous le dites, cela semble plus propre que de devoir créer un objet au préalable (et de devoir toujours le supprimer si vous souhaitez l'allouer sur le tas).

EMP
la source