Puis-je renvoyer une canalisation temporaire vers une opération de cuisinière?

9

Supposons que j'ai une generate_my_rangeclasse qui modélise un range(en particulier, est regular). Le code suivant est-il alors correct:

auto generate_my_range(int some_param) {    
  auto my_transform_op = [](const auto& x){ return do_sth(x); };
  return my_custom_rng_gen(some_param) | ranges::views::transform(my_transform_op);
}
auto cells = generate_my_range(10) | ranges::to<std::vector>;

Est my_custom_rng_gen(some_param)pris en valeur par le (premier) opérateur de canalisation, ou ai-je une référence pendant quand je quitte la generate_my_rangeportée?

Serait-ce la même chose avec l'appel fonctionnel ranges::views::transform(my_custom_rng_gen(some_param),my_transform_op)?

Serait-il correct si j'utilisais une référence lvalue? par exemple:

auto generate_my_range(int some_param) {
  auto my_transform_op = [](const auto& x){ return do_sth(x); };
  auto tmp_ref = my_custom_rng_gen(some_param);
  return tmp_ref | ranges::views::transform(my_transform_op);
}

Si les plages sont prises par des valeurs pour ces opérations, que dois-je faire si je passe une référence lvalue à un conteneur? Dois-je utiliser un ranges::views::all(my_container)motif?

Bérenger
la source
My_custom_rng_gen (some_param) est-il déjà délimité? Voulez-vous dire quelque chose comme godbolt.org/z/aTF8RN sans la prise (5)?
Porsche9II
@ Porsche9II Oui, c'est une gamme limitée. Disons que c'est un conteneur
Bérenger

Réponses:

4

Dans la bibliothèque de plages, il existe deux types d'opérations:

  • vues qui sont paresseuses et nécessitent que le conteneur sous-jacent existe.
  • actions qui sont désireuses, et produisent de nouveaux conteneurs en conséquence (ou modifiez ceux existants)

Les vues sont légères. Vous les transmettez par valeur et exigez que les conteneurs sous-jacents restent valides et inchangés.

De la documentation gammes-v3

Une vue est un wrapper léger qui présente une vue d'une séquence sous-jacente d'éléments d'une manière personnalisée sans la muter ou la copier. Les vues sont bon marché à créer et à copier et ont une sémantique de référence sans propriétaire.

et:

Toute opération sur la plage sous-jacente qui invalide ses itérateurs ou sentinelles invalidera également toute vue faisant référence à une partie de cette plage.

La destruction du conteneur sous-jacent invalide évidemment tous ses itérateurs.

Dans votre code, vous utilisez spécifiquement des vues - Vous utilisez ranges::views::transform. La pipe est simplement un sucre syntaxique qui facilite l'écriture telle qu'elle est. Vous devriez regarder la dernière chose dans le tuyau pour voir ce que vous produisez - dans votre cas, c'est une vue.

S'il n'y avait pas d'opérateur de tuyau, cela ressemblerait probablement à ceci:

ranges::views::transform(my_custom_rng_gen(some_param), my_transform_op)

s'il y avait plusieurs transformations connectées de cette façon, vous pouvez voir à quel point cela deviendrait laid.

Ainsi, si my_custom_rng_genproduit une sorte de conteneur, que vous transformez puis renvoyez, ce conteneur est détruit et vous avez des références pendantes de votre vue. Si my_custom_rng_genc'est une autre vue d'un conteneur qui vit en dehors de ces étendues, tout va bien.

Cependant, le compilateur doit être capable de reconnaître que vous appliquez une vue sur un conteneur temporaire et de vous rencontrer avec une erreur de compilation.

Si vous souhaitez que votre fonction renvoie une plage en tant que conteneur, vous devez explicitement «matérialiser» le résultat. Pour cela, utilisez l' ranges::toopérateur dans la fonction.


Mise à jour: Pour être plus explicite concernant votre commentaire "où la documentation indique-t-elle que la composition de la plage / des tuyaux prend et stocke une vue?"

La pipe est simplement un sucre syntaxique pour relier les choses dans une expression facile à lire. Selon la façon dont il est utilisé, il peut ou non renvoyer une vue. Cela dépend de l'argument de droite. Dans votre cas, c'est:

`<some range> | ranges::views::transform(...)`

Ainsi, l'expression renvoie tout ce qui views::transformretourne.

Maintenant, en lisant la documentation de la transformation:

Vous trouverez ci-dessous une liste des combinateurs de plage paresseux, ou vues, que Range-v3 fournit, et un texte explicatif sur la façon dont chacun est destiné à être utilisé.

[...]

views::transform

Étant donné une plage source et une fonction unaire, renvoyez une nouvelle plage où chaque élément de résultat est le résultat de l'application de la fonction unaire à un élément source.

Il renvoie donc une plage, mais comme il s'agit d'un opérateur paresseux, cette plage qu'il renvoie est une vue, avec toute sa sémantique.

CygnusX1
la source
D'accord. Ce qui est encore un peu mystérieux pour moi, c'est comment ça marche quand je passe un conteneur à la pipe (c'est-à-dire l'objet range créé par la composition). Il doit en quelque sorte stocker une vue du conteneur. Est-ce fini ranges::views::all(my_container)? Et si une vue est transmise au tuyau? Est-ce qu'il reconnaît qu'il a passé un conteneur ou une vue? En a-t-il besoin? Comment?
Bérenger
"Le compilateur devrait être capable de reconnaître que vous appliquez une vue sur un conteneur temporaire et vous frapper avec une erreur de compilation" C'est ce que je pensais aussi: si je fais quelque chose de stupide, cela signifie un contrat sur le type n'est pas remplie. Des trucs comme ça se font par range-v3. Mais dans ce cas, il n'y a absolument aucun problème. Il compile ET s'exécute. Il peut donc y avoir un comportement indéfini, mais il n'apparaît pas.
Bérenger
Pour être sûr si votre code fonctionne correctement par accident ou si tout va bien, j'aurais besoin de voir le contenu de my_custom_rng_gen. Comment exactement le tuyau et transforminteragir sous le capot n'est pas important. L'expression entière prend une plage comme argument (un conteneur ou une vue vers un conteneur) et renvoie une vue différente à ce conteneur. La valeur de retour ne sera jamais propriétaire du conteneur, car il s'agit d'une vue.
CygnusX1
1

Tiré de la documentation des gammes-v3 :

Les vues ont une [...] sémantique de référence sans propriétaire.

et

Avoir un objet à portée unique permet des pipelines d'opérations. Dans un pipeline, une gamme est paresseusement adaptée ou mutée avec impatience d'une manière ou d'une autre, avec le résultat immédiatement disponible pour une adaptation ou une mutation supplémentaire. L'adaptation paresseuse est gérée par les vues, et la mutation désireuse est gérée par les actions.

// taken directly from the the ranges documentation
std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){ return i % 2 == 1; })
              | views::transform([](int i){ return std::to_string(i); });
// rng == {"2","4","6","8","10"};

Dans le code ci-dessus, rng stocke simplement une référence aux données sous-jacentes et aux fonctions de filtrage et de transformation. Aucun travail n'est effectué jusqu'à ce que rng soit itéré.

Puisque vous avez dit que la plage temporaire peut être considérée comme un conteneur, votre fonction renvoie une référence pendante.

En d'autres termes, vous devez vous assurer que la plage sous-jacente survit à la vue, sinon vous êtes en difficulté.

Rumburak
la source
Oui, les vues n'appartiennent pas, mais où la documentation indique-t-elle que la composition de la plage / de la tuyauterie prend et stocke une vue? Il serait possible (et je pense, une bonne chose) d'avoir la politique suivante: stocker par valeur si la plage est donnée par une référence rvalue.
Bérenger
1
@ Bérenger J'ai ajouté un peu plus dans la documentation des gammes. Mais le point est vraiment: une vue n'est pas propriétaire . Peu importe que vous lui remettiez une valeur.
Rumburak