27 votes

Interface pour renvoyer un tas de valeurs

J'ai une fonction qui prend un nombre et renvoie à beaucoup de choses (par exemple, ints). Ce qui est le plus propre de l'interface? Quelques réflexions:

  1. De retour d'un vector<int>. Le vecteur serait copié à plusieurs reprises, ce qui est inefficace.
  2. De retour d'un vector<int>*. Mon getter doit désormais répartir le vecteur lui-même, ainsi que les éléments. Il y a tous les problèmes habituels de l'oms a gratuit le vecteur, le fait que vous ne pouvez pas allouer une fois et utilisez le même stockage pour beaucoup de différents appels à la lecture, etc. C'est pourquoi les algorithmes de la STL généralement éviter d'allouer de la mémoire, au lieu de vouloir qu'il est passé dans.
  3. De retour d'un unique_ptr<vector<int>>. Il est maintenant clair qui le supprime, mais nous avons encore d'autres problèmes.
  4. Prendre un vector<int> comme un paramètre de référence. La lecture peut - push_back() et l'appelant peut décider d' reserve() de l'espace. Cependant, ce qui devrait le getter faire si le passé- vector est non-vide? Ajouter? Remplacer par compensation d'abord? Affirmer que c'est vide? Ce serait bien si la signature de la fonction n'a accordé qu'une seule interprétation.
  5. Passer un begin et end itérateur. Maintenant nous avons besoin de retourner le nombre d'éléments effectivement écrit (qui peut être plus petite que souhaitée), et l'appelant doit être prudent de ne pas accéder à des éléments qui n'ont jamais été écrites.
  6. Avoir de la lecture prendre un iterator, et l'appelant peut passer un insert_iterator.
  7. Abandonner et de passer un char *. :)

37voto

Andy Prowl Points 62121

En C++11, où la sémantique de déplacement est pris en charge pour les conteneurs standard, vous devriez aller avec l'option 1.

Il rend la signature de votre fonction claire, la communication que vous voulez juste un vecteur d'entiers de retour, et il sera efficace, car aucune copie ne sera émis: le constructeur de déplacement de l' std::vector sera invoquée (ou, plus vraisemblablement, Nommé Valeur de Retour d'Optimisation sera appliquée, ce qui n'entraîne aucun déplacement et pas de copie):

std::vector<int> foo()
{
    std::vector<int> v;
    // Fill in v...
    return v;
}

De cette façon, vous n'aurez pas à traiter de questions telles que la propriété, inutile allocations dynamiques, et d'autres choses qui sont juste à polluer la simplicité de votre problème: de retour d'un tas de nombres entiers.

En C++03, vous mai voulez aller avec l'option 4 et prendre une lvalue référence à un non-const vecteur: conteneurs standard en C++03 ne sont pas les déplacer, et de la copie d'un vecteur peut être coûteux. Donc:

void foo(std::vector<int>& v)
{
    // Fill in v...
}

Cependant, même dans ce cas, vous devriez considérer si cette pénalité est vraiment important pour votre cas d'utilisation. Si elle n'est pas, vous pouvez opter pour une plus claire de la fonction de signature au détriment de certains cycles de PROCESSEUR.

Aussi, C++03 compilateurs sont capables d'exécuter Nommé Valeur de Retour d'Optimisation, de sorte que même si en théorie temporaire doit être la copie construit à partir de la valeur de retour, dans la pratique, aucune copie est susceptible de se produire.

11voto

Useless Points 18909

Vous l'avez écrit vous-même:

... C'est pourquoi les algorithmes de la STL généralement éviter d'allouer de la mémoire, au lieu de vouloir qu'il est passé dans

sauf que les algorithmes de la STL n'est pas typiquement "voulez mémoire du passé", ils opèrent sur des itérateurs à la place. C'est précisément afin de dissocier l'algorithme dans le récipient, ce qui donne lieu à:

option 8

découpler la création de valeur à partir de l'utilisation et de stockage de ces valeurs, en retournant un itérateur d'entrée.

La façon la plus simple est d'utiliser boost::function_input_iterator, mais une esquisse mécanisme est en dessous (surtout parce que j'etais en train de taper plus vite que la pensée).


Itérateur d'entrée de type

(utilise le C++11, mais vous pouvez remplacer l' std::function avec un pointeur de fonction ou tout simplement coder en dur la logique de génération):

#include <functional>
#include <iterator>
template <typename T>
class Generator: public std::iterator<std::input_iterator_tag, T> {
    int count_;
    std::function<T()> generate_;
public:
    Generator() : count_(0) {}
    Generator(int count, std::function<T()> func) : count_(count)
                                                  , generate_(func) {}
    Generator(Generator const &other) : count_(other.count_)
                                      , generate_(other.generate_) {}
    // move, assignment etc. etc. omitted for brevity
    T operator*() { return generate_(); }
    Generator<T>& operator++() {
        --count_;
        return *this;
    }
    Generator<T> operator++(int) {
        Generator<T> tmp(*this);
        ++*this;
        return tmp;
    }
    bool operator==(Generator<T> const &other) const {
        return count_ == other.count_;
    }
    bool operator!=(Generator<T> const &other) const {
        return !(*this == other);
    }
};

Exemple de générateur de fonction

(encore une fois, il est trivial de remplacer le lambda avec une ligne de la fonction en C++98, mais c'est moins de frappe)

#include <random>
Generator<int> begin_random_integers(int n) {
    static std::minstd_rand prng;
    static std::uniform_int_distribution<int> pdf;
    Generator<int> rv(n,
                      []() { return pdf(prng); }
                     );
    return rv;
}
Generator<int> end_random_integers() {
    return Generator<int>();
}

Exemple d'utilisation

#include <vector>
#include <algorithm>
#include <iostream>
int main()
{
    using namespace std;
    vector<int> out;

    cout << "copy 5 random ints into a vector\n";
    copy(begin_random_integers(5), end_random_integers(),
         back_inserter(out));
    copy(out.begin(), out.end(),
         ostream_iterator<int>(cout, ", "));

    cout << "\n" "print 2 random ints straight from generator\n";
    copy(begin_random_integers(2), end_random_integers(),
         ostream_iterator<int>(cout, ", "));

    cout << "\n" "reuse vector storage for 3 new ints\n";
    out.clear();
    copy(begin_random_integers(3), end_random_integers(),
         back_inserter(out));
    copy(out.begin(), out.end(),
         ostream_iterator<int>(cout, ", "));
}

4voto

yngum Points 3453

renvoie vector<int> , il ne sera pas copié, il sera déplacé.

4voto

Yakk Points 31636

En C++11, le droit de réponse est le retour de l' std::vector<int> est de retour, en s'assurant qu'il sera explicitement ou implicitement déplacé. (Préférence implicite déplacer, parce qu'explicites déplacer peut bloquer certaines optimisations)

De manière amusante, si vous êtes inquiet au sujet de la réutilisation de la mémoire tampon, le plus simple est de jeter un paramètre facultatif qui prend un std::vector<int> , en valeur, comme ceci:

std::vector<int> get_stuff( int how_many, std::vector<int> retval = std::vector<int>() ) {
  // blah blah
  return retval;
}

et, si vous avez un préaffectés tampon de la bonne taille, juste std::move dans la get_stuff fonction et il sera utilisé. Si vous n'avez pas de préaffectés tampon de la bonne taille, ne pas passer une std::vector dans.

Live exemple: http://ideone.com/quqnMQ

Je ne suis pas certain si cela va bloquer NRVO/RVO, mais il n'y a pas de raison fondamentale pour qu'il le devrait, et le déplacement d'un std::vector est assez bon marché que vous ne serez probablement pas de soins si il ne bloque NRVO/RVO de toute façon.

Toutefois, vous ne pourrez pas retourner un std::vector<int> - peut-être vous voulez juste pour itérer sur les éléments en question.

Dans ce cas, il est un moyen facile et une voie difficile.

Le plus simple est d'exposer un for_each_element( Lambda ) méthode:

#include <iostream>
struct Foo {
  int get_element(int i) const { return i*2+1; }
  template<typename Lambda>
  void for_each_element( int up_to, Lambda&& f ) {
    for (int i = 0; i < up_to; ++i ) {
      f( get_element(i) );
    }
  }
};
int main() {
  Foo foo;
  foo.for_each_element( 7, [&](int e){
    std::cout << e << "\n";
  });
}

et éventuellement utiliser un std::function si vous devez masquer la mise en œuvre de l' for_each.

La dure serait de revenir à un générateur ou une paire d'itérateurs qui génèrent les éléments en question.

Ces deux éviter l'inutile, de l'allocation de la mémoire tampon lorsque vous ne voulez traiter avec les éléments un à un, et si la génération des valeurs en question est cher (on pourrait exiger de la traversée de la mémoire

En C++98, je prendrais vector& et clear() il.

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