4 votes

Comment la `partial_sum` de range-v3 ne contredit-elle pas la sémantique de référence non propriétaire?

Considérez Comment écrire un pipeline de plage utilisant des conteneurs temporaires?. La question est de savoir comment construire une vue transformant chaque élément T à l'aide d'une fonction donnée

std::vector f(T t);

tout en étant conforme à la restriction (en empruntant la réponse principale là-bas) que

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

En gros, toutes les réponses là-bas semblent être d'accord pour dire que, en raison de cette restriction, cela ne peut pas être fait via une vue.


Je ne comprends pas comment cela s'intègre dans la bibliothèque prenant en charge partial_sum.

Considérez l'entier glorifié suivant:

#include 
#include 
#include 
#include 

using namespace ranges;

struct glorified_int {
    explicit glorified_int(int i) : m_i{std::make_shared(i)} {}
    operator int() const { return *m_i; }
    std::shared_ptr m_i;
};

glorified_int operator+(const glorified_int &lhs, const glorified_int &rhs) {
    glorified_int ret{(int)lhs + (int)rhs};
    return ret;
}

Cela emballe essentiellement un int dans une classe le stockant dans un std::shared_ptr, permettant d'initialiser, d'extraire et d'ajouter. En ce qui concerne la sémantique de référence sans propriétaire, je ne vois pas de différence fondamentale entre cela et un conteneur tel que std::vector.

La plage ne semble pas avoir de problème à appliquer partial_sum à cela, cependant:

int main() {
    std::vector vi{ glorified_int{1}, glorified_int{2} };
    for(const auto &ps: vi | view::partial_sum())
        std::cout << ps << std::endl;

Affiche

$ ./a.out
1 
3

N'est-ce pas (l'entier glorifié) 3 un temporaire ici? Il ne fait certainement pas partie de la séquence d'origine. De plus, une somme partielle est une transformation avec état, évidemment, alors comment la plage peut-elle garantir que

Les vues sont bon marché à créer et à copier, et possèdent une sémantique de référence sans propriétaire.

La vue est aussi coûteuse à copier que l'objet d'accumulation.

Remarquez qu'il n'y a aucun problème à chaîner cela plus loin (c'est-à-dire, ce n'est pas une action):

    vi | view::partial_sum() | view::take(10);

Quelle est alors la différence?


Code complet

#include 
#include 
#include 
#include 

using namespace ranges;

struct glorified_int {
    explicit glorified_int(int i) : m_i{std::make_shared(i)} {}
    operator int() const { return *m_i; }
    std::shared_ptr m_i;
};

glorified_int operator+(const glorified_int &lhs, const glorified_int &rhs) {
    glorified_int ret{(int)lhs + (int)rhs};
    return ret;
}

int main() {
    std::vector vi{ glorified_int{1}, glorified_int{2} };
    for(const auto &ps: vi | view::partial_sum())
        std::cout << ps << std::endl;
    vi | view::partial_sum() | view::take(10);
}

4voto

Barry Points 45207

Ce qui fait d'une vue une vue, c'est qu'elle ne prend pas ou ne nécessite pas la propriété, la copie ou la modification des éléments de la plage d'entrée. Mais une vue n'est pas obligée de n'avoir aucun état. Même take() ou filter() possèdent quelque état (un compteur et un prédicat, respectivement).

Dans ce cas spécifique, partial_sum n'a pas besoin de posséder aucun des éléments de la plage d'entrée. C'est le travail de la plage d'entrée. Il n'a pas non plus besoin de les copier ou de les modifier. Il doit simplement suivre son propre état - la somme en cours (un optional) et la fonction binaire effectuant la somme (un plus). Il possède un de ses propres objets, mais cet objet existe en dehors de la plage d'entrée entièrement. Cela en fait toujours une vue, juste une vue avec état.

Vous écrivez:

La vue est aussi coûteuse à copier que l'objet d'accumulation.

C'est vrai. Mais c'est aussi vrai pour de nombreuses vues. transform() est aussi coûteux à copier que la fonction que nous utilisons pour transformer la vue, peut-être avez-vous une monstruosité étatique, chère et allocatrice de mémoire énorme.

Quand Eric parle de bon marché à créer et à copier, je pense qu'il parle dans le contexte de la création et de la copie de la plage d'entrée entière pour produire une nouvelle plage. Alors que partial_sum() doit garder la somme en cours, ce qui n'est pas bon marché dans votre cas car cet élément nécessite une allocation, c'est quand même bien moins cher que d'écrire un partial_sum basé sur une action:

// version bon marché
for(const auto &ps: vi | view::partial_sum()) { ... }

// version coûteuse
std::vector partial_sums;
if (!vi.empty()) {
    auto it = vi.begin();
    partial_sums.emplace_back(*it++);
    for (; it != vi.end(); ++it) {
        partial_sums.emplace_back(*it + partial_sums.back());
    }
}
for (const auto &ps : partial_sums) { ... }

De toute évidence, nous n'avons pas besoin de tout le vecteur partial_sums pour faire ce que nous voulons (si nous en avions besoin, eh bien, il n'y a pas moyen de contourner cela). La vue nous offre un moyen bon marché de, eh bien, voir les sommes partielles.

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