Imaginons que nous ayons une structure pour contenir 3 doubles avec des fonctions membres:
struct Vector {
double x, y, z;
// ...
Vector &negate() {
x = -x; y = -y; z = -z;
return *this;
}
Vector &normalize() {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
// ...
};
C'est un peu artificiel pour la simplicité, mais je suis sûr que vous êtes d'accord pour dire qu'un code similaire existe. Les méthodes vous permettent d'enchaîner facilement, par exemple:
Vector v = ...;
v.normalize().negate();
Ou même:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Maintenant, si nous avons fourni les fonctions begin () et end (), nous pourrions utiliser notre Vector dans un nouveau style for loop, disons pour faire une boucle sur les 3 coordonnées x, y et z (vous pouvez sans doute construire plus d'exemples "utiles" en remplaçant Vector par eg String):
Vector v = ...;
for (double x : v) { ... }
On peut même faire:
Vector v = ...;
for (double x : v.normalize().negate()) { ... }
et aussi:
for (double x : Vector{1., 2., 3.}) { ... }
Cependant, ce qui suit (il me semble) est cassé:
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Bien que cela semble être une combinaison logique des deux utilisations précédentes, je pense que cette dernière utilisation crée une référence pendante alors que les deux précédentes sont tout à fait correctes.
- Est-ce correct et largement apprécié?
- Quelle partie de ce qui précède est la "mauvaise" partie, qui devrait être évitée?
- Le langage serait-il amélioré en modifiant la définition de la boucle for basée sur la plage de sorte que les temporels construits dans l'expression for existent pendant la durée de la boucle?
Réponses:
Oui, votre compréhension des choses est correcte.
La mauvaise partie prend une référence de valeur l à un temporaire renvoyé par une fonction et la lie à une référence de valeur r. C'est aussi grave que ça:
La
Vector{1., 2., 3.}
durée de vie du temporaire ne peut pas être étendue car le compilateur n'a aucune idée que la valeur renvoyée par lui faitnormalize
référence.Ce serait très incompatible avec le fonctionnement de C ++.
Cela empêcherait-il certains pièges faits par des personnes utilisant des expressions chaînées sur des temporaires ou diverses méthodes d'évaluation paresseuse pour les expressions? Oui. Mais cela nécessiterait également du code de compilateur dans des cas spéciaux, ainsi que la raison pour laquelle il ne fonctionne pas avec d' autres constructions d'expression.
Une solution beaucoup plus raisonnable serait un moyen d'informer le compilateur que la valeur de retour d'une fonction est toujours une référence à
this
, et par conséquent, si la valeur de retour est liée à une construction à extension temporaire, elle étendrait le temporaire correct. C'est une solution au niveau de la langue.Actuellement (si le compilateur le prend en charge), vous pouvez faire en sorte qu'il
normalize
ne puisse pas être appelé sur un temporaire:Cela entraînera
Vector{1., 2., 3.}.normalize()
une erreur de compilation, alors quev.normalize()
cela fonctionnera correctement. De toute évidence, vous ne pourrez pas faire des choses correctes comme celle-ci:Mais vous ne pourrez pas non plus faire de mauvaises choses.
Alternativement, comme suggéré dans les commentaires, vous pouvez faire en sorte que la version de référence rvalue renvoie une valeur plutôt qu'une référence:
Si
Vector
était un type avec des ressources réelles à déplacer, vous pouvez utiliser à laVector ret = std::move(*this);
place. L'optimisation de la valeur de retour nommée rend cela raisonnablement optimal en termes de performances.la source
delete
vous pourriez fournir une opération alternative qui renvoie une rvalue:Vector normalize() && { normalize(); return std::move(*this); }
(je crois que l'appel à l'normalize
intérieur de la fonction sera envoyé à la surcharge lvalue, mais quelqu'un devrait le vérifier :)&
/&&
qualification des méthodes. Est-ce de C ++ 11 ou est-ce une extension de compilateur propriétaire (peut-être répandue). Donne des possibilités intéressantes.Ce n'est pas une limitation de la langue, mais un problème avec votre code. L'expression
Vector{1., 2., 3.}
crée un temporaire, mais lanormalize
fonction renvoie une référence lvalue . Étant donné que l'expression est une lvalue , le compilateur suppose que l'objet sera vivant, mais comme il s'agit d'une référence à un temporaire, l'objet meurt après l'évaluation de l'expression complète, de sorte que vous vous retrouvez avec une référence pendante.Désormais, si vous modifiez votre conception pour renvoyer un nouvel objet par valeur plutôt qu'une référence à l'objet actuel, il n'y aurait aucun problème et le code fonctionnerait comme prévu.
la source
const
référence prolongerait-elle la durée de vie de l'objet dans ce cas?normalize()
une fonction de mutation sur un objet existant. Ainsi la question. Le fait qu'un temporaire ait une «durée de vie prolongée» lorsqu'il est utilisé dans le but précis d'une itération, et non autrement, est, à mon avis, une erreur déroutante.const&
) a sa durée de vie prolongée.Vector & r = Vector{1.,2.,3.}.normalize();
. Votre conception a cette limitation, et cela signifie que soit vous êtes prêt à retourner par valeur (ce qui peut avoir du sens dans de nombreuses circonstances, et plus encore avec rvalue-references et move ), ou bien vous devez gérer le problème à la place de call: créez une variable appropriée, puis utilisez-la dans la boucle for. A noter également que l'expressionVector v = Vector{1., 2., 3.}.normalize().negate();
crée deux objets ...T const& f(T const&);
tout va bien.T const& t = f(T());
est tout à fait bien. Et puis, dans un autre TU, vous découvrez celaT const& f(T const& t) { return t; }
et vous pleurez ... Si celaoperator+
fonctionne sur des valeurs, c'est plus sûr ; alors le compilateur peut optimiser la copie (Want Speed? Pass by Values), mais c'est un bonus. La seule liaison des temporaires que j'autoriserais est la liaison aux références r-values, mais les fonctions doivent alors renvoyer des valeurs pour la sécurité et s'appuyer sur Copy Elision / Move Semantics.IMHO, le deuxième exemple est déjà imparfait. Que les opérateurs de modification retournent
*this
est pratique de la manière que vous avez mentionnée: cela permet le chaînage des modificateurs. Il peut être utilisé simplement pour transmettre le résultat de la modification, mais cela est sujet aux erreurs car cela peut facilement être négligé. Si je vois quelque chose commeJe ne soupçonnerais pas automatiquement que les fonctions se modifient
v
comme un effet secondaire. Bien sûr, ils le pourraient , mais ce serait déroutant. Donc, si je devais écrire quelque chose comme ça, je m'assurerais que celav
reste constant. Pour votre exemple, j'ajouterais des fonctions gratuitespuis écrivez les boucles
et
C'est l'OMI mieux lisible, et c'est plus sûr. Bien sûr, cela nécessite une copie supplémentaire, mais pour les données allouées au tas, cela pourrait probablement être fait dans une opération de déplacement C ++ 11 bon marché.
la source