La pratique de renvoyer une variable de référence C ++ est-elle mauvaise?


341

C'est un peu subjectif je pense; Je ne sais pas si l'opinion sera unanime (j'ai vu beaucoup d'extraits de code où les références sont retournées).

Selon un commentaire sur cette question que je viens de poser, concernant l'initialisation des références , renvoyer une référence peut être mauvais car, [si je comprends bien], il est plus facile de manquer de la supprimer, ce qui peut entraîner des fuites de mémoire.

Cela m'inquiète, car j'ai suivi des exemples (à moins que j'imagine des choses) et l'ai fait à plusieurs endroits ... Ai-je mal compris? Est-ce mal? Si oui, à quel point le mal?

Je pense qu'en raison de mon mélange de pointeurs et de références, combiné au fait que je suis nouveau en C ++ et à une confusion totale sur ce qu'il faut utiliser quand, mes applications doivent être un enfer de fuite de mémoire ...

De plus, je comprends que l'utilisation de pointeurs intelligents / partagés est généralement acceptée comme le meilleur moyen d'éviter les fuites de mémoire.


Ce n'est pas mal si vous écrivez des fonctions / méthodes de type getter.
John Z. Li

Réponses:


411

En général, le renvoi d'une référence est parfaitement normal et se produit tout le temps.

Si tu veux dire:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

C'est toutes sortes de mal. La pile allouée idisparaîtra et vous ne faites référence à rien. C'est aussi mal:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Parce que maintenant, le client doit finalement faire l'étrange:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Notez que les références rvalue ne sont encore que des références, donc toutes les applications malveillantes restent les mêmes.

Si vous souhaitez allouer quelque chose qui dépasse la portée de la fonction, utilisez un pointeur intelligent (ou en général, un conteneur):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

Et maintenant, le client stocke un pointeur intelligent:

std::unique_ptr<int> x = getInt();

Les références sont également acceptables pour accéder à des éléments dont vous savez que la durée de vie est maintenue ouverte à un niveau supérieur, par exemple:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Ici, nous savons qu'il est correct de renvoyer une référence i_car tout ce qui nous appelle gère la durée de vie de l'instance de classe, il i_vivra donc au moins aussi longtemps.

Et bien sûr, il n'y a rien de mal à simplement:

int getInt() {
   return 0;
}

Si la durée de vie doit être laissée à l'appelant et que vous calculez simplement la valeur.

Résumé: vous pouvez renvoyer une référence si la durée de vie de l'objet ne se termine pas après l'appel.


21
Ce sont tous de mauvais exemples. Le meilleur exemple d'utilisation correcte est lorsque vous renvoyez une référence à un objet qui a été transmis. Opérateur ala <<
Arelius

171
Pour des raisons de postérité, et pour tout programmeur plus récent, les pointeurs ne sont pas mauvais . Les pointeurs vers la mémoire dynamique ne sont pas non plus mauvais. Ils ont tous deux leur place légitime en C ++. Les pointeurs intelligents devraient certainement être votre référence par défaut en matière de gestion dynamique de la mémoire, mais votre pointeur intelligent par défaut devrait être unique_ptr, pas shared_ptr.
Jamin Gray

12
Modifier les approbateurs: n'approuvez pas les modifications si vous ne pouvez pas en garantir l'exactitude. J'ai annulé la modification incorrecte.
GManNickG

7
Pour des raisons de postérité, et pour tout programmeur plus récent, ne pas écrirereturn new int .
Courses de légèreté en orbite le

3
Pour des raisons de postérité, et pour tout programmeur plus récent, il suffit de renvoyer le T de la fonction. RVO s'occupe de tout.
Chaussure du

64

Non, non, mille fois non.

Ce qui est mal, c'est de faire référence à un objet alloué dynamiquement et de perdre le pointeur d'origine. Lorsque vous newun objet, vous assumez l'obligation d'avoir une garantie delete.

Mais jetez un œil à, par exemple, operator<<qui doit renvoyer une référence, ou

cout << "foo" << "bar" << "bletch" << endl ;

ne fonctionnera pas.


23
J'ai rétrogradé parce que cela ne répond pas à la question (dans laquelle OP a clairement indiqué qu'il connaît la nécessité de la suppression) ni ne répond à la crainte légitime que le renvoi d'une référence à un objet freestore puisse prêter à confusion. Soupir.

4
La pratique du retour d'un objet de référence n'est pas mauvaise. Ergo no. La peur qu'il exprime est une peur correcte, comme je le souligne dans le deuxième graf.
Charlie Martin

Vous ne l'avez pas fait. Mais cela ne vaut pas mon temps.

2
Iraimbilanja @ À propos des «non», je m'en fiche. mais ce post a souligné une information importante qui manquait à GMan.
Kobor42

48

Vous devez renvoyer une référence à un objet existant qui ne disparaît pas immédiatement et où vous ne prévoyez aucun transfert de propriété.

Ne renvoyez jamais une référence à une variable locale ou à une telle, car elle ne sera pas là pour être référencée.

Vous pouvez renvoyer une référence à quelque chose d'indépendant de la fonction, que vous ne vous attendez pas à ce que la fonction appelante prenne la responsabilité de la suppression. C'est le cas pour la operator[]fonction typique .

Si vous créez quelque chose, vous devez renvoyer une valeur ou un pointeur (normal ou intelligent). Vous pouvez renvoyer une valeur librement, car elle va dans une variable ou une expression dans la fonction appelante. Ne renvoyez jamais un pointeur sur une variable locale, car il disparaîtra.


1
Excellente réponse mais pour "Vous pouvez renvoyer un temporaire comme référence const." Le code suivant se compilera mais se bloquera probablement car le temporaire est détruit à la fin de l'instruction return: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker: C ++ a des règles étranges pour les références aux temporaires, où la durée de vie temporaire peut être prolongée. Je suis désolé, je ne le comprends pas assez bien pour savoir s'il couvre votre cas.
Mark Ransom

3
@Mark: Oui, il a des règles étranges. La durée de vie d'un temporaire ne peut être prolongée qu'en initialisant une référence const (qui n'est pas un membre de classe) avec elle; il vit ensuite jusqu'à ce que l'arbitre sorte du champ d'application. Malheureusement, le renvoi d'une référence const n'est pas couvert. Le retour d'un temp par valeur est cependant sûr.
j_random_hacker

Voir la norme C ++, 12.2, paragraphe 5. Voir également le gourou errant de la semaine de Herb Sutter à herbsutter.wordpress.com/2008/01/01/… .
David Thornley

4
@David: Lorsque le type de retour de la fonction est "T const &", ce qui se passe en réalité, c'est que l'instruction return convertit implicitement la température, qui est de type T, en "T const &" conformément au 6.6.3.2 (une conversion légale mais une qui ne prolonge pas la durée de vie), puis le code appelant initialise la référence de type "T const &" avec le résultat de la fonction, également de type "T const &" - ​​encore une fois, un processus légal mais ne prolongeant pas la durée de vie. Résultat final: pas de prolongation de la durée de vie et beaucoup de confusion. :(
j_random_hacker

26

Je ne trouve pas les réponses satisfaisantes, je vais donc ajouter mes deux cents.

Analysons les cas suivants:

Utilisation erronée

int& getInt()
{
    int x = 4;
    return x;
}

C'est évidemment une erreur

int& x = getInt(); // will refer to garbage

Utilisation avec des variables statiques

int& getInt()
{
   static int x = 4;
   return x;
}

C'est vrai, car les variables statiques existent tout au long de la durée de vie d'un programme.

int& x = getInt(); // valid reference, x = 4

Ceci est également assez courant lors de l'implémentation d'un modèle Singleton

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Usage:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

Les opérateurs

Les conteneurs de bibliothèque standard dépendent fortement de l'utilisation d'opérateurs qui renvoient une référence, par exemple

T & operator*();

peut être utilisé dans les cas suivants

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Accès rapide aux données internes

Il y a des moments où & peut être utilisé pour un accès rapide aux données internes

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

avec utilisation:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

CEPENDANT, cela peut conduire à un piège comme celui-ci:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!

Le retour de la référence à une variable statique peut conduire à un comportement indésirable. Par exemple, considérez un opérateur de multiplication qui renvoie une référence à un membre statique, ce qui se traduira toujours par true:If((a*b) == (c*d))
SebNag

Container::data()la mise en œuvre de devrait lirereturn m_data;
Xeaz

Cela a été très utile, merci! @Xeaz ne provoquerait-il pas des problèmes avec l'appel d'ajout?
Andrew

@SebTu Pourquoi voudriez-vous même faire ça?
thorhunter

@Andrew Non, c'était un shenanigan de syntaxe. Si, par exemple, vous avez renvoyé un type de pointeur, vous utiliserez l'adresse de référence pour créer et renvoyer un pointeur.
thorhunter

14

Ce n'est pas mal. Comme beaucoup de choses en C ++, c'est bien s'il est utilisé correctement, mais il y a de nombreux pièges que vous devez savoir lorsque vous l'utilisez (comme retourner une référence à une variable locale).

Il y a de bonnes choses qui peuvent être réalisées avec cela (comme map [name] = "hello world")


1
Je suis juste curieux, à quoi ça sert map[name] = "hello world"?
faux nom d'utilisateur

5
@wrongusername La syntaxe est intuitive. Avez-vous déjà essayé d'incrémenter le nombre d'une valeur stockée dans un HashMap<String,Integer>en Java? : P
Mehrdad Afshari

Haha, pas encore, mais en regardant les exemples de HashMap, ça a l'air assez noueux: D
mauvais nom d'utilisateur

Problème que j'ai eu avec ceci: la fonction renvoie une référence à un objet dans un conteneur, mais le code de la fonction appelante l'a affecté à une variable locale. Puis modifié certaines propriétés de l'objet. Problème: l'objet d'origine dans le conteneur est resté intact. Le programmeur
néglige

10

"renvoyer une référence est mauvais parce que, simplement [si je comprends bien], il est plus facile de manquer de la supprimer"

Pas vrai. Le renvoi d'une référence n'implique pas de sémantique de propriété. C'est juste parce que vous faites cela:

Value& v = thing->getTheValue();

... ne signifie pas que vous possédez maintenant la mémoire mentionnée par v;

Cependant, c'est un code horrible:

int& getTheValue()
{
   return *new int;
}

Si vous faites quelque chose comme ça parce que "vous n'avez pas besoin d'un pointeur sur cette instance" alors: 1) juste déréférencer le pointeur si vous avez besoin d'une référence, et 2) vous aurez éventuellement besoin du pointeur, car vous devez faire correspondre un nouveau avec une suppression, et vous avez besoin d'un pointeur pour appeler la suppression.


7

Il y a deux cas:

  • référence const - bonne idée, parfois, en particulier pour les objets lourds ou les classes proxy, optimisation du compilateur

  • référence non constante - mauvaise idée, parfois, rompt les encapsulations

Les deux partagent le même problème - peut potentiellement pointer vers un objet détruit ...

Je recommanderais d'utiliser des pointeurs intelligents pour de nombreuses situations où vous devez renvoyer une référence / un pointeur.

Notez également ce qui suit:

Il existe une règle formelle - la norme C ++ (section 13.3.3.1.4 si vous êtes intéressé) stipule qu'un temporaire ne peut être lié qu'à une référence const - si vous essayez d'utiliser une référence non const, le compilateur doit le signaler comme une erreur.


1
la référence non const ne rompt pas nécessairement l'encapsulation. considérer vector :: operator []

c'est un cas très spécial ... c'est pourquoi j'ai dit parfois, bien que je devrais vraiment réclamer LA PLUPART DU TEMPS :)

Donc, vous dites que la mise en œuvre normale de l'opérateur en indice est un mal nécessaire? Je ne suis ni en désaccord ni d'accord avec cela; comme je n'en suis pas plus sage.
Nick Bolton le

Je ne dis pas cela, mais cela peut être mauvais s'il est mal utilisé :))) vector :: at devrait être utilisé chaque fois que possible ....

hein? vector :: at renvoie également une référence non constante.

4

Non seulement ce n'est pas mal, c'est parfois essentiel. Par exemple, il serait impossible d'implémenter l'opérateur [] de std :: vector sans utiliser une valeur de retour de référence.


Ah, oui bien sûr; Je pense que c'est pourquoi j'ai commencé à l'utiliser; comme lorsque j'ai implémenté pour la première fois l'opérateur d'indice [], j'ai réalisé l'utilisation de références. Je suis amené à croire que c'est de facto.
Nick Bolton le

Curieusement, vous pouvez implémenter operator[]un conteneur sans utiliser de référence ... et c'est le std::vector<bool>cas. (Et crée un vrai gâchis dans le processus)
Ben Voigt

@BenVoigt mmm, pourquoi un gâchis? Le retour d'un proxy est également un scénario valide pour les conteneurs avec un stockage complexe qui ne correspond pas directement aux types externes (comme ::std::vector<bool>vous l'avez mentionné).
Sergey.quixoticaxis.Ivanov

1
@ Sergey.quixoticaxis.Ivanov: Le gâchis est que l'utilisation std::vector<T>dans le code du modèle est cassée, si Tcela est possible bool, car std::vector<bool>a un comportement très différent des autres instanciations. C'est utile, mais il aurait dû être donné son propre nom et non une spécialisation de std::vector.
Ben Voigt du

@BenVoight Je suis d'accord sur le point concernant une décision étrange de faire une spécialisation "vraiment spéciale" mais j'ai senti que votre commentaire original implique que le retour d'un proxy est étrange en général.
Sergey.quixoticaxis.Ivanov

2

Ajout à la réponse acceptée:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Je dirais que cet exemple n'est pas correct et devrait être évité si possible. Pourquoi? Il est très facile de se retrouver avec une référence pendante .

Pour illustrer ce point avec un exemple:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

entrer dans la zone de danger:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!

1

la référence de retour est généralement utilisée dans la surcharge d'opérateur en C ++ pour les objets de grande taille, car le retour d'une valeur nécessite une opération de copie. (en cas de surcharge du pérateur, nous n'utilisons généralement pas le pointeur comme valeur de retour)

Mais la référence de retour peut provoquer un problème d'allocation de mémoire. Dans la mesure où une référence au résultat sera passée hors de la fonction en tant que référence à la valeur de retour, la valeur de retour ne peut pas être une variable automatique.

si vous voulez utiliser le renvoi de référence, vous pouvez utiliser un tampon d'objet statique. par exemple

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

de cette façon, vous pouvez utiliser le renvoi de référence en toute sécurité.

Mais vous pouvez toujours utiliser le pointeur au lieu de la référence pour renvoyer une valeur dans functiong.


0

Je pense que l'utilisation de référence comme valeur de retour de la fonction est beaucoup plus simple que d'utiliser le pointeur comme valeur de retour de la fonction. Deuxièmement, il serait toujours sûr d'utiliser une variable statique à laquelle se réfère la valeur de retour.


0

La meilleure chose est de créer un objet et de le passer comme paramètre de référence / pointeur à une fonction qui alloue cette variable.

Allouer un objet dans la fonction et le renvoyer comme référence ou pointeur (le pointeur est plus sûr cependant) est une mauvaise idée en raison de la libération de mémoire à la fin du bloc fonction.


-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

La fonction getPtr peut accéder à la mémoire dynamique après la suppression ou même à un objet nul. Ce qui peut provoquer de mauvaises exceptions d'accès. Au lieu de cela, getter et setter doivent être implémentés et leur taille vérifiée avant de revenir.


-2

La fonction comme lvalue (aka, retour de références non const) doit être supprimée de C ++. C'est terriblement peu intuitif. Scott Meyers voulait un min () avec ce comportement.

min(a,b) = 0;  // What???

ce qui n'est pas vraiment une amélioration

setmin (a, b, 0);

Ce dernier est même plus logique.

Je me rends compte que la fonction lvalue est importante pour les flux de style C ++, mais il convient de souligner que les flux de style C ++ sont terribles. Je ne suis pas le seul à penser cela ... si je me souviens, Alexandrescu avait un grand article sur la façon de faire mieux, et je pense que boost a également essayé de créer une meilleure méthode d'E / S sécurisée.


2
Bien sûr, c'est dangereux, et il devrait y avoir une meilleure vérification des erreurs du compilateur, mais sans cela, certaines constructions utiles ne pourraient pas être faites, par exemple operator [] () dans std :: map.
j_random_hacker

2
Le renvoi de références non const est en fait incroyablement utile. vector::operator[]par exemple. Préférez-vous écrire v.setAt(i, x)ou v[i] = x? Ce dernier est FAR supérieur.
Miles Rout

1
@MilesRout J'irais à v.setAt(i, x)tout moment. C'est bien supérieur.
scravy

-2

Je suis tombé sur un vrai problème où il était en effet mauvais. Un développeur a essentiellement renvoyé une référence à un objet dans un vecteur. C'était mauvais !!!

Les détails complets sur lesquels j'ai écrit en janvier: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Si vous devez modifier la valeur d'origine dans le code appelant, vous devez renvoyer une référence. Et ce n'est en fait ni plus ni moins dangereux que de retourner un itérateur à un vecteur - les deux sont invalides si des éléments sont ajoutés ou supprimés du vecteur.
j_random_hacker

Ce problème particulier a été provoqué par la tenue d'une référence à un élément vectoriel, puis la modification de ce vecteur d'une manière qui invalide la référence: Page 153, section 6.2 de «Bibliothèque standard C ++: un didacticiel et une référence» - Josuttis, se lit comme suit: «Insertion ou la suppression d'éléments invalide les références, les pointeurs et les itérateurs qui font référence aux éléments suivants. Si une insertion provoque une réallocation, elle invalide toutes les références, les itérateurs et les pointeurs "
Trent

-15

À propos du code horrible:

int& getTheValue()
{
   return *new int;
}

Donc, en effet, le pointeur de mémoire a perdu après le retour. Mais si vous utilisez shared_ptr comme ça:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

La mémoire n'est pas perdue après le retour et sera libérée après l'affectation.


12
Il est perdu car le pointeur partagé sort de la portée et libère l'entier.

le pointeur n'est pas perdu, l'adresse de la référence est le pointeur.
dgsomerton
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.