Est-ce un piège connu de C ++ 11 pour les boucles?

89

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?
ndkrempel
la source
Pour une raison quelconque, je me souviens d'une question très similaire posée auparavant, j'ai oublié comment elle était appelée.
Pubby
Je considère cela comme un défaut de langage. La vie des temporaires n'est pas étendue au corps entier de la boucle for, mais uniquement pour la configuration de la boucle for. Ce n'est pas seulement la syntaxe de plage qui en souffre, la syntaxe classique en souffre également. À mon avis, la durée de vie des temporaires dans l'instruction init devrait s'étendre sur toute la durée de vie de la boucle.
edA-qa mort-ora-y
1
@ edA-qamort-ora-y: J'ai tendance à convenir qu'il y a un léger défaut de langage qui se cache ici, mais je pense que c'est spécifiquement le fait que l'extension de la durée de vie se produit implicitement chaque fois que vous liez directement un temporaire à une référence, mais pas dans aucun autre situation - cela semble être une solution à moitié cuite au problème sous-jacent des durées de vie temporaires, même si cela ne veut pas dire qu'il soit évident quelle serait une meilleure solution. Peut-être une syntaxe explicite «d'extension de durée de vie» lors de la construction du temporaire, qui le fait durer jusqu'à la fin du bloc actuel - qu'en pensez-vous?
ndkrempel
@ edA-qamort-ora-y: ... cela revient au même que de lier le temporaire à une référence, mais présente l'avantage d'être plus explicite pour le lecteur qu'une `` extension de durée de vie '' se produit, en ligne (dans une expression , plutôt que d'exiger une déclaration distincte), et ne vous obligeant pas à nommer l'intérimaire.
ndkrempel
1
duplication possible d' un objet temporaire en fonction de la plage pour
ildjarn

Réponses:

64

Est-ce correct et largement apprécié?

Oui, votre compréhension des choses est correcte.

Quelle partie de ce qui précède est la "mauvaise" partie, qui devrait être évitée?

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:

auto &&t = Vector{1., 2., 3.}.normalize();

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 fait normalizeréférence.

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?

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:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Cela entraînera Vector{1., 2., 3.}.normalize()une erreur de compilation, alors que v.normalize()cela fonctionnera correctement. De toute évidence, vous ne pourrez pas faire des choses correctes comme celle-ci:

Vector t = Vector{1., 2., 3.}.normalize();

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:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Si Vectorétait un type avec des ressources réelles à déplacer, vous pouvez utiliser à la Vector ret = std::move(*this);place. L'optimisation de la valeur de retour nommée rend cela raisonnablement optimal en termes de performances.

Nicol Bolas
la source
1
La chose qui pourrait rendre cela plus un "gotcha" est que la nouvelle boucle for cache syntaxiquement le fait que la liaison de référence se déroule sous les couvertures - c'est-à-dire que c'est beaucoup moins flagrant que vos exemples "tout aussi mauvais" ci-dessus. C'est pourquoi il semblait plausible de suggérer la règle d'extension de durée de vie supplémentaire, juste pour la nouvelle boucle for.
ndkrempel
1
@ndkrempel: Oui, mais si vous proposez une fonctionnalité de langage pour résoudre ce problème (et que vous devez donc attendre au moins 2017), je préférerais que ce soit plus complet, quelque chose qui pourrait résoudre le problème d'extension temporaire partout .
Nicol Bolas
3
+1. Sur la dernière approche, plutôt que deletevous pourriez fournir une opération alternative qui renvoie une rvalue: Vector normalize() && { normalize(); return std::move(*this); }(je crois que l'appel à l' normalizeintérieur de la fonction sera envoyé à la surcharge lvalue, mais quelqu'un devrait le vérifier :)
David Rodríguez - dribeas
3
Je n'ai jamais vu cela &/ &&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.
Christian Rau
1
@ChristianRau: C'est nouveau dans C ++ 11, et analogue aux qualifications C ++ 03 "const" et "volatile" des fonctions membres non statiques, en ce sens qu'il qualifie "this" dans un certain sens. g ++ 4.7.0 ne le prend pas en charge cependant.
ndkrempel
25

pour (double x: Vector {1., 2., 3.}. normalize ()) {...}

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 la normalizefonction 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.

David Rodríguez - Dribeas
la source
1
Une constréférence prolongerait-elle la durée de vie de l'objet dans ce cas?
David Stone
5
Ce qui briserait la sémantique clairement souhaitée d' 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.
Andy Ross
2
@AndyRoss: Pourquoi? Toute liaison temporaire à une référence de valeur r (ou const&) a sa durée de vie prolongée.
Nicol Bolas
2
@ndkrempel: Pourtant, pas une limitation de la gamme à base de boucle, le même problème viendrait si vous liez à une référence: 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'expression Vector v = Vector{1., 2., 3.}.normalize().negate();crée deux objets ...
David Rodríguez - dribeas
1
@ DavidRodríguez-dribeas: le problème avec la liaison à const-reference est le suivant: 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 cela T const& f(T const& t) { return t; }et vous pleurez ... Si cela operator+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.
Matthieu M.
4

IMHO, le deuxième exemple est déjà imparfait. Que les opérateurs de modification retournent *thisest 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 comme

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

Je ne soupçonnerais pas automatiquement que les fonctions se modifient vcomme 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 cela vreste constant. Pour votre exemple, j'ajouterais des fonctions gratuites

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

puis écrivez les boucles

for( double x : negated(normalized(v)) ) { ... }

et

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

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é.

à gauche
la source
Merci. Comme d'habitude, les choix sont nombreux. Une situation dans laquelle votre suggestion peut ne pas être viable est si Vector est un tableau (non alloué au tas) de 1000 doubles, par exemple. Un compromis d'efficacité, de facilité d'utilisation et de sécurité d'utilisation.
ndkrempel
2
Oui, mais il est rarement utile d'avoir des structures de taille> ≈100 sur la pile, de toute façon.
gauche vers