36 votes

Pourquoi ne pas toujours affecter les valeurs de retour de const référence?

Disons que j'ai une fonction:

Foo GetFoo(..)
{
  ...
}

Supposons que nous ne savons comment cette fonction est mise en œuvre, ni de l'intérieur de Foo (il peut être un objet très complexe, par exemple). Toutefois, nous savons que la fonction est de retour Toto en valeur et que nous voulons utiliser cette valeur de retour comme const.

Question: Serait-il toujours une bonne idée de stocker la valeur de retour de cette fonction en tant que const &?

const Foo& f = GetFoo(...);

au lieu de,

const Foo f = GetFoo(...);

Je sais que les compilateurs ferait valeur de retour optimisations et peut-être le déplacement de l'objet au lieu de le copier, donc à la fin, const & pourrait ne pas avoir tous les avantages. Cependant, ma question est, existe-il des inconvénients? Pourquoi ne pas simplement de développer la mémoire musculaire de toujours utiliser const & pour stocker les valeurs de retour étant donné que je n'ai pas à compter sur les optimisations du compilateur et le fait que même opération de déplacement peut être coûteux pour les objets complexes.

S'étendant à l'extrême, pourquoi ne devrais-je pas toujours utiliser const & pour toutes les variables qui sont immuables dans mon code? Par exemple,

const int& a = 2;
const int& b = 2;
const int& c = c + d;

En plus d'être plus verbeux, sont les inconvénients?

30voto

Yakk Points 31636

L'appel de élision d'une "optimisation" est une idée fausse. Les compilateurs sont autorisés à ne pas le faire, mais ils sont également autorisés à mettre en oeuvre a+b entier plus comme une séquence d'opérations bit à bit avec manuel transporter.

Un compilateur qui ne qui serait hostile: le compilateur qui refuse de les éluder.

Élision n'est pas comme les "autres" optimisations, comme ceux qui s'appuient sur le cas de la règle (comportement peut changer tant qu'il se comporte comme si la norme dicte). Élision peut modifier le comportement du code.

Pourquoi l'aide d' const & ou même rvalue && est une mauvaise idée, les références sont des alias à un objet. Avec soit, vous n'avez pas (local) de garantir que l'objet ne soit pas manipulée par ailleurs. En fait, si la fonction renvoie un &, const& ou &&, l'objet doit exister ailleurs, avec une autre identité dans la pratique. Donc votre "local" valeur est plutôt une référence à un inconnu lointain de l'état: ce qui rend le comportement difficile à comprendre.

Valeurs, d'autre part, ne peut pas être un alias. Vous pouvez former ces alias après la création, mais un const valeur locale ne peut pas être modifié en vertu de la norme, même s'il existe un alias pour elle.

Raisonner sur les objets locaux est facile. Raisonner sur les objets distribués est dur. Les références sont distribués de type: si vous devez choisir entre un cas de référence ou de la valeur et il n'est pas évident de performance coût de la valeur, toujours choisir des valeurs.

Pour être concret:

Foo const& f = GetFoo();

pourrait être une référence de la liaison à un temporaire de type Foo ou dérivé retourné à partir de GetFoo(), ou une référence lié à quelque chose d'autre stockés dans GetFoo(). Nous ne pouvons pas dire à partir de cette ligne.

Foo const& GetFoo();

vs

Foo GetFoo();

faire f ont des significations différentes, en effet.

Foo f = GetFoo();

toujours créer une copie. Rien qui ne modifie pas "par" f modifiera f (à moins que son ctor passé un pointeur sur lui-même pour quelqu'un d'autre, bien sûr).

Si nous avons

const Foo f = GetFoo();

nous avons même la garantie que la modification de la (non-mutable pièces d') f est un comportement indéfini. On peut supposer f est immuable, et, en fait, le compilateur va le faire.

Dans l' const Foo& des cas, la modification d' f peut être défini comportement si le stockage sous-jacent est non-const. Donc nous ne pouvons pas assumer f est immuable, et le compilateur ne supposons qu'il est immuable, si l'on peut examiner l'ensemble du code qui a valablement dérivé des pointeurs ou des références à d' f et de déterminer qu'aucun d'entre eux muter (même si vous venez de passer autour de const Foo&, si l'objet d'origine était un non-const Foo, il est légal d' const_cast<Foo&> et de la modifier).

En bref, ne pas prématuré pessimize et d'assumer élision "ne marchera pas." Il y a très peu de courant compilateurs ne pas éluder sans explicitement de l'éteindre, et vous avez presque certainement ne serez pas en la construction d'un projet sérieux sur eux.

28voto

David Schwartz Points 70129

Ces différences sémantiques et si vous demandez quelque chose d'autre que vous voulez, vous serez dans le pétrin si vous l'obtenez. Considérer ce code:

#include <stdio.h>

class Bar
{
    public:
    Bar() { printf ("Bar::Bar\n"); }
    ~Bar() { printf ("Bar::~Bar\n"); }
    Bar(const Bar&) { printf("Bar::Bar(const Bar&)\n"); }
    void baz() const { printf("Bar::Baz\n"); }
};

class Foo
{
    Bar bar;

    public:
    Bar& getBar () { return bar; }
    Foo() { }
};

int main()
{
    printf("This is safe:\n");
    {
        Foo *x = new Foo();
        const Bar y = x->getBar();
        delete x;
        y.baz();
    }
    printf("\nThis is a disaster:\n");
    {
        Foo *x = new Foo();
        const Bar& y = x->getBar();
        delete x;
        y.baz();
    }
    return 0;
}

La sortie est:

C'est sûr:
Bar::Bar
Bar::Bar(const Bar&)
Bar::~Bar
Bar::Baz
Bar::~Bar

C'est une catastrophe:
Bar::Bar
Bar::~Bar
Bar::Baz

Avis nous appelons Bar::Baz après l' Bar est détruit. Oups.

Demander ce que vous voulez, de cette façon, vous n'êtes pas vissée si vous obtenez ce que vous demandez.

13voto

Mark Ransom Points 132545

S'appuyant sur ce @David Schwartz a dit dans les commentaires, vous devez être sûr que la sémantique ne change pas. Il ne suffit pas que vous avez l'intention de traiter la valeur comme immuable, la fonction que vous l'avez obtenu à partir de doit de le traiter comme immuable trop ou vous allez avoir une surprise.

image.SetPixel(x, y, white_pixel);
const Pixel &pix = image.GetPixel(x, y);
image.SetPixel(x, y, black_pixel);
cout << pix;

4voto

Leon Points 20011

La différence sémantique entre const C& et const C dans le cas (lors de la sélection d'un type pour une variable) peut affecter votre programme dans les cas énumérés ci-dessous. Ils doivent être pris en compte lors de la rédaction d'un nouveau code, mais aussi lors de l'entretien ultérieur, étant donné que certaines modifications au code source peut changer lorsqu'une définition de la variable appartient à cette classification.

Initialiseur est une lvalue de exactement le type C

const C& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) introduit un objet indépendant, dans une certaine mesure permise par la copie de la sémantique du type C), alors que (2) crée un alias vers un autre objet et est soumis à toutes les modifications qui se passe à cet objet (y compris de sa fin de vie).

Initialiseur est une lvalue d'un type dérivé de C

struct D : C { ... };
const D& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) est une des tranches de la version de ce qui a été renvoyé de foo(). (2) est lié à la dérivée de l'objet et peut profiter des avantages de comportement polymorphique (le cas échéant), bien que le risque d'être mordu par des problèmes d'aliasing.

Initialiseur est une rvalue d'un type dérivé de C

struct D : C { ... };
D foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

Pour (1), ce n'est pas différent du cas précédent. En ce qui concerne (2), il n'y a plus d'aliasing! La référence constante est liée à la temporaire de type dérivé, dont la durée de vie s'étend jusqu'à la fin de l'affichage de la portée, avec le bon destructeur (~D()) est automatiquement appelée. (2) peuvent profiter des avantages de polymorphisme, mais paie le prix des ressources supplémentaires consommées par D par rapport à l' C.

Initialiseur est une rvalue d'un type convertible en une lvalue de type C

struct B {
    C c;
    operator const C& () const { return c; }
};
const B foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) rend sa copie et s'en va, tandis que (2) est en difficulté en commençant immédiatement à partir de la prochaine déclaration, puisque c'alias un sous-objet d'un objet mort!

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