2010-01-26 9 views
8

J'ai vu de nombreux arguments que l'utilisation d'une valeur de retour est préférable à des paramètres out. Je suis convaincu des raisons pour lesquelles je dois les éviter, mais je ne suis pas sûr de savoir si je suis dans une situation où c'est inévitable.Comment éviter les paramètres?

Première partie de ma question est: Quels sont certains de vos moyens préférés/communs de se déplacer en utilisant un paramètre out? Stuff le long des lignes: Man, dans les critiques par les pairs, je vois toujours d'autres programmeurs le font quand ils auraient pu le faire facilement de cette façon.

Deuxième partie de ma question traite de certains cas spécifiques que j'ai rencontrés où je voudrais éviter un paramètre out mais ne peut pas penser à un moyen propre de le faire.

Exemple 1: J'ai une classe avec une copie coûteuse que je voudrais éviter. Le travail peut être fait sur l'objet et cela construit l'objet pour être coûteux à copier. Le travail de construction des données n'est pas non plus trivial non plus. Actuellement, je vais passer cet objet dans une fonction qui va modifier l'état de l'objet. Pour moi, c'est préférable à la nouveauté de l'objet interne à la fonction de travail et de le renvoyer, car cela me permet de garder les choses sur la pile.

class ExpensiveCopy //Defines some interface I can't change. 
{ 
public: 
    ExpensiveCopy(const ExpensiveCopy toCopy){ /*Ouch! This hurts.*/ }; 
    ExpensiveCopy& operator=(const ExpensiveCopy& toCopy){/*Ouch! This hurts.*/}; 

    void addToData(SomeData); 
    SomeData getData(); 
} 

class B 
{ 
public: 
    static void doWork(ExpensiveCopy& ec_out, int someParam); 
    //or 
    // Your Function Here. 
} 

En utilisant ma fonction, je me appelle code comme ceci:

const int SOME_PARAM = 5; 
ExpensiveCopy toModify; 
B::doWork(toModify, SOME_PARAM); 

J'aimerais avoir quelque chose comme ceci:

ExpensiveCopy theResult = B::doWork(SOME_PARAM); 

Mais je ne sais pas si cela est possible.

Deuxième exemple: J'ai un tableau d'objets. Les objets dans le tableau sont un type complexe, et j'ai besoin de travailler sur chaque élément, travail que je voudrais garder séparé de la boucle principale qui accède à chaque élément. Le code ressemble à ceci:

std::vector<ComplexType> theCollection; 
for(int index = 0; index < theCollection.size(); ++index) 
{ 
    doWork(theCollection[index]); 
} 

void doWork(ComplexType& ct_out) 
{ 
    //Do work on the individual element. 
} 

Des suggestions sur la façon de gérer certaines de ces situations? Je travaille principalement en C++, mais je suis intéressé de voir si d'autres langages facilitent une installation plus facile. J'ai rencontré RVO comme une solution possible, mais j'ai besoin d'en lire plus à ce sujet et cela ressemble à une fonctionnalité spécifique au compilateur.

+0

La sémantique d'un paramètre de référence non-const est qu'il sera (peut-être) modifié dans la fonction. Si vous n'alliez pas le changer, vous le passeriez comme référence de const, après tout. – Bill

+0

* "mais je suis intéressé de voir si d'autres langages facilitent une installation plus simple" * - les langages comme Java et C# n'ont tout simplement pas ce problème car ils donnent toujours des références aux objets qui les entourent. –

+0

Merveilleuses réponses de tous. J'avais le sentiment que dans mes exemples précis, il n'y aurait pas beaucoup d'autres options.J'ai vu des messages sur le SO de la part de personnes qui sont si catégoriques sur la façon dont ils évitent les paramètres à tout prix. J'espérais que peut-être l'un d'entre eux pourrait expliquer comment ils réalisent un tel exploit. Comme pour RVO, je suis quelque peu hésitant à utiliser des fonctionnalités spécifiques au compilateur, car il serait difficile d'expliquer à mes supérieurs pourquoi je dois redessiner la moitié de mon code car il ne fonctionne plus en raison d'une fonctionnalité manquante suite à un changement de compilateur/mise à niveau. –

Répondre

7

Je ne sais pas pourquoi vous essayez d'éviter de passer des références ici. C'est à peu près ces situations que la sémantique passe-par-référence existe.

Le code

static void doWork(ExpensiveCopy& ec_out, int someParam); 

semble parfaitement bien pour moi.

Si vous voulez vraiment le modifier alors vous avez deux options

  1. DoWork Move donc c'est qu'il est un membre de ExpensiveCopy (que vous dites que vous ne pouvez pas le faire, alors c'est out)
  2. retourner un pointeur (intelligent) à partir de doWork au lieu de le copier.(Que vous ne voulez pas faire ce que vous voulez garder les choses sur la pile)
  3. sur RVO (Fiez que d'autres ont souligné est soutenu par à peu près tous les compilateurs modernes)
+0

D'accord. Une seule chose, l'option 1 que vous mentionnez n'est pas disponible, puisque l'OP dit que «ExpensiveCopy» ne peut pas être modifié. – stakx

+0

@stakx, vous avez raison, j'ai raté ça. Va corriger cela – Glen

+0

@stakx: Je sais que l'OP a mentionné qu'il ne peut pas modifier la classe. Mais pourquoi ne peut-il pas le sous-classer et y ajouter la méthode doWork()? – slebetman

1

À moins que vous allez en bas de la route "tout est immuable", qui ne s'accorde pas trop bien avec C++. vous ne pouvez pas facilement éviter les paramètres. La bibliothèque standard C++ les utilise, et ce qui est assez bon est suffisant pour moi.

+0

Je suis totalement en désaccord avec la dernière déclaration: malheureusement, la bibliothèque standard C++ n'est pas proche de l'idéal (juste du haut de ma tête: folie std :: string, flux trop élaborés, etc.) Je ne veux pas démarrer holiwar mais je suppose que ce n'est pas la meilleure idée pour justifier quelque chose à cause d'un exemple bien connu. –

+2

Bien sûr, il a ses verrues, mais en général je le trouve extrêmement puissant et facile à utiliser - je souhaite seulement que mon propre code soit aussi bien conçu. –

0

En ce qui concerne votre premier exemple: return value optimization permet souvent à l'objet retourné d'être créé directement sur place, au lieu d'avoir à copier l'objet. Tous les compilateurs modernes le font.

3

Chaque compilateur utile ne RVO (retour optimisation de la valeur) si les optimisations sont activées, ainsi le fait de suivre efficacement donne pas lieu à la copie:

Expensive work() { 
    // ... no branched returns here 
    return Expensive(foo); 
} 

Expensive e = work(); 

Dans certains cas, les compilateurs peuvent appliquer NRVO, appelée optimisation de la valeur de retour, ainsi:

Expensive work() { 
    Expensive e; // named object 
    // ... no branched returns here 
    return e; // return named object 
} 

Ceci cependant n'est pas exactement fiable, ne fonctionne que dans des cas plus triviaux et devrait être testé. Si vous n'êtes pas prêt à tester chaque cas, utilisez simplement des paramètres externes avec des références dans le second cas.

2

IMO la première chose que vous devriez vous demander est de savoir si la copie ExpensiveCopy est vraiment trop chère. Et pour répondre à cela, vous aurez généralement besoin d'un profileur. Sauf si un profileur vous dit que la copie est vraiment un goulot d'étranglement, il suffit d'écrire le code qui est plus facile à lire: ExpensiveCopy obj = doWork(param);.

Bien sûr, il y a bien des cas où les objets ne peuvent pas être copiés pour des raisons de performances ou autres. Puis Neil's answer s'applique.

0

Sur quelle plate-forme travaillez-vous?

La raison pour laquelle je demande est que beaucoup de gens ont suggéré l'optimisation de la valeur de retour, qui est une optimisation de compilateur très pratique présente dans presque tous les compilateurs. De plus, Microsoft et Intel mettent en œuvre ce qu'ils appellent l'optimisation de la valeur de retour nommée, ce qui est encore plus pratique.

Dans l'optimisation de la valeur de retour standard, votre instruction return est un appel au constructeur d'un objet, qui indique au compilateur d'éliminer les valeurs temporaires (pas nécessairement l'opération de copie).

Dans l'optimisation de la valeur de retour nommée, vous pouvez renvoyer une valeur par son nom et le compilateur fera la même chose. L'avantage de NRVO est que vous pouvez effectuer des opérations plus complexes sur la valeur créée (comme les fonctions d'appel sur celle-ci) avant de la renvoyer.

Bien qu'aucun d'entre eux n'élimine vraiment une copie coûteuse si vos données renvoyées sont très volumineuses, elles aident. En ce qui concerne l'évitement de la copie, la seule façon de procéder consiste à utiliser des pointeurs ou des références car votre fonction doit modifier les données à l'endroit où vous voulez qu'elles se retrouvent. Cela signifie que vous voulez probablement avoir une paramètre passe-par-référence.

Aussi je figure que je devrais faire remarquer que la référence de passage est très commune dans le code de haute performance spécifiquement pour cette raison. La copie de données peut être incroyablement coûteuse, et c'est souvent ce que les gens oublient en optimisant leur code.

2

En plus de tous les commentaires ici, je vous signale que, en C++ 0x vous souhaitez rarement utiliser le paramètre de sortie à des fins d'optimisation - en raison de Constructors Déplacer (voir here)

+0

J'ai récemment essayé de mettre en place un système de prise en charge des lectures de fichiers qui ont utilisé une fois la sémantique de déplacement. J'ai bien travaillé, mais j'ai fini par me brûler à cause d'incompatibilités avec différents compilateurs. J'ai dû tout changer à des paramètres qui était vraiment décevant. –

0

Pour autant que je peux voir , les raisons de préférer les valeurs de retour aux paramètres out sont plus claires et fonctionnent avec une programmation purement fonctionnelle (vous pouvez obtenir de bonnes garanties si une fonction dépend uniquement des paramètres d'entrée, retourne une valeur et n'a pas d'effets secondaires). La première raison est stylistique et, à mon avis, elle n'est pas si importante. Le second n'est pas un bon ajustement avec C++. Par conséquent, je n'essaierais pas de déformer quoi que ce soit pour éviter les paramètres.

Le fait est que certaines fonctions doivent retourner plusieurs choses, et dans la plupart des langues, cela suggère des paramètres. Common Lisp a multiple-value-bind et multiple-value-return, dans lequel une liste de symboles est fournie par la liaison et une liste de valeurs est renvoyée. Dans certains cas, une fonction peut renvoyer une valeur composite, telle qu'une liste de valeurs qui sera ensuite déconstruite, et ce n'est pas un gros problème pour une fonction C++ de renvoyer un std::pair. Renvoyer plus de deux valeurs de cette manière en C++ devient gênant. Il est toujours possible de définir une structure, mais la définir et la créer sera souvent plus compliquée que les paramètres externes.

Dans certains cas, la valeur de retour est surchargée. En C, getchar() retourne un int, avec l'idée qu'il y a plus de valeurs int que char (vrai dans toutes les implémentations que je connais, fausses dans certaines que je peux facilement imaginer), donc une des valeurs peut être utilisée pour de-fichier. atoi() renvoie un entier, soit l'entier représenté par la chaîne qu'il a passé, soit zéro s'il n'y en a pas, donc il retourne la même chose pour "0" et "frog". (Si vous voulez savoir s'il y avait une valeur int ou non, utilisez strtol(), qui a un paramètre out.)

Il y a toujours la technique de lancer une exception en cas d'erreur, mais pas toutes les valeurs de retour multiples sont des erreurs, et toutes les erreurs ne sont pas exceptionnelles. Par conséquent, les valeurs de retour surchargées entraînent des problèmes, les retours de valeurs multiples ne sont pas faciles à utiliser dans toutes les langues et les retours uniques n'existent pas toujours. Lancer une exception est souvent inapproprié. L'utilisation de paramètres est très souvent la solution la plus propre.

0

Demandez-vous pourquoi vous avez une méthode qui effectue un travail sur cet objet cher à copier en premier lieu. Supposons que vous ayez un arbre, voulez-vous envoyer l'arbre dans une méthode de construction ou lui donner sa propre méthode de construction? Des situations comme celle-ci apparaissent constamment lorsque vous avez un peu de conception, mais ont tendance à se replier sur eux-mêmes lorsque vous l'avez en panne. Je sais que dans la pratique, nous ne pouvons pas toujours changer tous les objets, mais passer des paramètres est une opération à effets secondaires, et il est beaucoup plus difficile de comprendre ce qui se passe, et vous n'avez jamais vraiment pour le faire (sauf comme forcé en travaillant dans les cadres de code des autres).

Parfois, c'est plus facile, mais ce n'est certainement pas souhaitable de l'utiliser sans raison (si vous avez souffert de quelques grands projets où il y a toujours une demi-douzaine de paramètres, vous saurez ce que je veux dire).

Questions connexes