2008-09-18 7 views
4

Si je vous écris une bibliothèque et j'ai une fonction qui doit retourner une séquence de valeurs, je pouvais faire quelque chose comme:API C++ pour les séquences de retour d'une manière générique

std::vector<int> get_sequence(); 

Cependant, cela nécessite la l'utilisateur de la bibliothèque pour utiliser le conteneur std :: vector <> plutôt que de leur permettre d'utiliser le conteneur qu'ils souhaitent utiliser. En outre, il peut ajouter une copie supplémentaire du tableau retourné (selon que le compilateur pourrait l'optimiser ou non), ce qui pourrait avoir un impact négatif sur les performances.

Vous pourriez théoriquement permettre l'utilisation de conteneurs arbitraires (et éviter la copie supplémentaire inutile) en faisant une fonction basé sur un modèle qui prend un début et une iter fin:

template<class T_iter> void get_sequence(T_iter begin, T_iter end); 

La fonction serait alors stocker les valeurs de séquence dans la gamme donnée par les itérateurs. Mais le problème avec ceci est qu'il vous oblige à connaître la taille de la séquence afin que vous ayez suffisamment d'éléments entre begin et end pour stocker toutes les valeurs dans la séquence.

Je pensais à une interface telle que:

template<T_insertIter> get_sequence(T_insertIter inserter); 

qui exige que le T_insertIter soit un insert iterator (par exemple créé avec std::back_inserter(my_vector)), mais cela semble trop facile à abuser car le compilateur heureusement accepter l'itérateur non inséré mais se comporterait incorrectement au moment de l'exécution.

Y a-t-il une meilleure pratique pour concevoir des interfaces génériques qui renvoient des séquences de longueur arbitraire?

Répondre

0

std::list<int> est légèrement plus agréable, IMO. Notez que cela ne nécessiterait pas de copie supplémentaire des données dans la liste, car seuls les pointeurs sont copiés.

Cela dépend entièrement de vos consommateurs. Si vous pouvez vous attendre à ce qu'ils soient des développeurs C++, donnez-leur l'une des classes de conteneurs std, dis-je.

La seule autre chose qui se produit à moi est que vous feriez ceci:

void get_sequence(std::tr1::function<void(int)> f); 

Ensuite, l'appelant peut utiliser std::tr1::bind pour effectuer votre appel de fonction get_sequence quelle que soit la fonction sur tout objet (ou non) qu'ils vouloir. Vous continuez à appeler f pour chaque élément que vous créez.

+0

'vector' contient également seulement un pointeur sur les données, et tout le contenu doit être copié afin d'empêcher l'aliasing. La même chose est vraie pour les listes. En outre, pourquoi est-ce "plus agréable, IMO"? Ce n'est pas du tout subjectif! Les listes sont bien adaptées lorsqu'elles nécessitent une insertion et une suppression rapides au milieu, et essentiellement pour rien d'autre. –

0

Vous pouvez faire quelque chose comme

template<typename container> 
container get_sequence(); 

et exiger que le type de conteneur fourni est conforme à une interface standard (comme avoir un push_back membre et peut-être réserve, de sorte que l'utilisateur de votre interface peut utiliser vecteur/deque /liste). Pourquoi avez-vous besoin de votre interface pour être indépendante du conteneur?

3

Scott Meyers dans son "Effective STL" donne un bon raisonnement pour pas en essayant de rendre votre code indépendant du conteneur, peu importe la force de la tentation. Fondamentalement, les conteneurs sont destinés à une utilisation complètement différente: vous ne voulez probablement pas stocker votre sortie dans la carte ou l'ensemble (ce ne sont pas des conteneurs d'intervalle), donc vous êtes à gauche avec vecteur, liste et deque, et pourquoi souhaitez-vous avoir un vecteur où vous avez besoin de liste et vice versa?Ils sont complètement différents, et vous aurez de meilleurs résultats en utilisant toutes les fonctionnalités de l'un d'entre eux que d'essayer de faire fonctionner les deux. Eh bien, il suffit de lire "Effective STL": ça vaut le coup.

Si vous savez quelque chose sur votre conteneur, cependant, vous pouvez envisager de faire quelque chose comme


template void get_sequence(T_Container & container) 
{ 
    //... 
    container.assign(iter1, iter2); 
    //... 
} 

ou peut-être


template void get_sequence(T_Container & container) 
{ 
    //... 
    container.resize(size); 
    //use push_back or whatever 
    //... 
} 

ou même de contrôler ce que vous faites avec une stratégie, comme


class AssignStrategy // for stl 
{ 
public: 
    template 
    void fill(T_Container & container, T_Container::iterator it1, T_Container::iterator it2){ 
    container.assign(it1, it2); 
    } 
}; 

class ReserveStrategy // for vectors and stuff 
{ 
public: 
    template 
    void fill(T_Container & container, T_Container::iterator it1, T_Container::iterator it2){ 
    container.reserve(it2 - it1); 
    while(it1 != it2) 
     container.push_back(*it1++); 
    } 
}; 


template 
void get_sequence(T_Container & container) 
{ 
    //... 
    T_FillStrategy::fill(container, iter1, iter2); 
    //... 
} 
+0

Bien sûr, je suis d'accord avec le point de base, mais la bibliothèque standard n'est pas le seul fournisseur de classes de conteneurs. Par exemple, quelqu'un utilisant la bibliothèque en conjonction avec Qt pourrait souhaiter utiliser QVector au lieu de std :: vector, etc. Mon but n'était pas vraiment de supporter des types de conteneurs complètement différents. – jonner

+1

Bien sûr, je suis d'accord avec votre point de base, mais je souhaite vraiment que les gens de Qt commencent à utiliser STD./moi sourit. –

0

Vous pouvez envoyer statiquement sur le type d'itérateur en utilisant iterator_traits

Quelque chose comme ceci:

template<T_insertIter> get_sequence(T_insertIter inserter) 
{ 
    return get_sequence(inserter, typename iterator_traits<Iterator>::iterator_category()); 
} 

template<T_insertIter> get_sequence(T_insertIter inserter, input_iterator_tag); 
6

Have get_sequence retourner une classe (personnalisée) forward_iterator qui génère la séquence sur demande. (Il peut également s'agir d'un type d'itérateur plus avancé comme bidirectional_iterator si cela est pratique pour votre séquence.)

Ensuite, l'utilisateur peut copier la séquence dans le type de conteneur souhaité. Ou, ils peuvent simplement boucler directement sur votre itérateur et ignorer le conteneur entièrement.

Vous aurez besoin d'une sorte d'itérateur d'extrémité. Sans savoir exactement comment vous générez la séquence, il est difficile de dire exactement comment vous devriez l'implémenter. Une façon serait pour votre classe itérateur d'avoir une fonction membre statique qui retourne un itérateur final, comme:

static const my_itr& end() { static const my_itr e(...); return e; }; 

... représente tout ce que les paramètres dont vous avez besoin pour créer l'itérateur final (qui pourrait utiliser un constructeur privé). Ensuite, votre boucle ressemblerait à ceci:

for (my_itr i = get_sequence(); i != my_itr::end(); ++i) { ... } 

Voici un exemple trivial d'une classe qui génère iterator avant une suite d'entiers consécutifs. Évidemment, cela pourrait facilement être transformé en un itérateur d'accès bidirectionnel ou aléatoire, mais je voulais garder l'exemple petit.

#include <iterator> 

class integer_sequence_itr 
    : public std::iterator<std::forward_iterator_tag, int> 
{ 
private: 
    int i; 

public: 
    explicit integer_sequence_itr(int start) : i(start) {}; 

    const int& operator*() const { return i; }; 
    const int* operator->() const { return &i; }; 

    integer_sequence_itr& operator++() { ++i; return *this; }; 
    integer_sequence_itr operator++(int) 
    { integer_sequence_itr copy(*this); ++i; return copy; }; 

    inline bool operator==(const integer_sequence_itr& rhs) const 
    { return i == rhs.i; }; 

    inline bool operator!=(const integer_sequence_itr& rhs) const 
    { return i != rhs.i; }; 
}; // end integer_sequence_itr 

//Example: Print the integers from 1 to 10. 
#include <iostream> 

int main() 
{ 
    const integer_sequence_itr stop(11); 

    for (integer_sequence_itr i(1); i != stop; ++i) 
    std::cout << *i << std::endl; 

    return 0; 
} // end main 
+0

Ceci est une solution intéressante, cependant, ne fonctionnera pas dans certains cas. Cela fonctionnera bien si vous avez toute la séquence mise en cache dans la classe ou s'il est bon marché de générer un seul élément dans la séquence. Sinon, cela pourrait être très inefficace. – jonner

+1

Un itérateur avant seul ne permet pas à l'utilisateur de vérifier la fin de la séquence. Le retour d'une plage fonctionnerait bien (c'est-à-dire une paire d'itérateurs donnant 'begin' et' end'). –

+0

C'est la meilleure solution: fournissez les fonctions 'begin()' et 'end()' pour retourner votre propre 'iterator'. Si vous n'avez pas la totalité de la séquence en cache et qu'il n'est pas bon marché de générer un seul élément, vous devrez ajouter des informations supplémentaires dans votre type d'itérateur pour faciliter la génération d'un seul élément. L'itérateur peut être bien plus qu'un pointeur, si cela peut vous aider. –

2

Une chose à porter une attention particulière à vous est si par bibliothèque une DLL ou dire similaire. Ensuite, il peut y avoir des problèmes si le consommateur de bibliothèque, disons une application, est construit avec un autre compilateur que la bibliothèque elle-même.

Considérons le cas où, dans votre exemple, vous renvoyez un std::vector<> par valeur. La mémoire sera alors allouée dans le contexte de la bibliothèque, mais désallouée dans le contexte de l'application. Deux compilateurs différents peuvent allouer/désallouer différemment, ce qui peut provoquer des dégâts.

+0

Légèrement hors sujet: En C++, vous avez de bonnes chances d'obtenir la garantie que les bibliothèques que vous écrivez seront compilées avec le même compilateur utilisé pour l'application client, ou n'exposeraient pas de code structuré ou structuré dans votre interface (Dans certains cas travail passé, nous avons produit trois binaires pour chaque bibliothèque que nous avons écrit, un par compilateur que nous devions prendre en charge). – paercebal

3

Heu ... Juste mes deux cents, mais:

void get_sequence(std::vector<int> & p_aInt); 

Cela supprimerait le rendement potentiel par problème de copie. Maintenant, si vous voulez vraiment éviter d'imposer un conteneur, vous pouvez essayer quelque chose comme:

template <typename T> 
void get_sequence(T & p_aInt) 
{ 
    p_aInt.push_back(25) ; // Or whatever you need to add 
} 

Ce compilerait seulement pour des vecteurs, des listes et deque (et contenants similaires).Si vous voulez un ensemble de LARGET des conteneurs possibles, le code serait:

template <typename T> 
void get_sequence(T & p_aInt) 
{ 
    p_aInt.insert(p_aInt.end(), 25) ; // Or whatever you need to add 
} 

Mais comme dit par d'autres postes, vous devez accepter de limiter votre interface à un type de conteneurs seulement.

1

Si vous gérez déjà la mémoire de votre séquence, vous pouvez renvoyer une paire d'itérateurs à utiliser par l'appelant dans la boucle for ou dans un appel d'algorithme.

Si la séquence renvoyée doit gérer sa propre mémoire, les choses sont plus compliquées. Vous pouvez utiliser la solution de @ paercebal, ou vous pouvez implémenter vos propres itérateurs qui maintiennent shared_ptr dans la séquence qu'ils répètent.

0

Pour la sortie de séquences, je vois deux options. La première est quelque chose comme

template <typename OutputIter> 
void generate_sequence(OutputIter out) 
{ 
    //... 
    while (...) { *out = ...; ++out; } 
} 

La seconde est quelque chose comme

struct sequence_generator 
{ 
    bool has_next() { ... } 
    your_type next() { mutate_state(); return next_value; } 

private: 
    // some state 
}; 

que vous voulez transformer en une norme C++ iterator (en utilisant boost::iterator_facade pour plus de commodité) pour l'utiliser dans les algorithmes standards (copy, transform, ...). Regardez également boost::transform_iterator, combiné avec quelques itérateurs renvoyant des entiers en séquence.

+0

La seconde n'est rien d'autre qu'un itérateur C++ avec une autre syntaxe. Pourquoi ne pas utiliser la syntaxe établie? –

+0

parce qu'il est terriblement difficile de bien faire les choses. Mais je mentionne 'boost :: iterator_facade'. –

0

Vous pouvez transmettre un foncteur à votre fonction qui accepte une seule valeur. Le foncteur serait alors responsable de stocker la valeur dans n'importe quel conteneur que vous utilisez à ce moment.

struct vector_adder { 
    vector_adder(std::vector<int>& v) : v(v) {} 
    void operator()(int n) { v.push_back(n); } 
    std::vector<int>& v; 
}; 

void gen_sequence(boost::function< void(int) > f) { 
    ... 
    f(n); 
    ... 
} 

main() { 
    std::vector<int> vi; 
    gen_sequence(vector_adder(vi)); 
} 

Remarque: J'utilise boost.function ici pour définir le paramètre functor. Vous n'avez pas besoin de boost pour être capable de faire cela. Cela rend le processus beaucoup plus simple.

Vous pouvez également utiliser un pointeur de fonction au lieu d'un foncteur, mais je ne le recommande pas. C'est une erreur et il n'y a pas de moyen facile de lier des données.

En outre, si votre compilateur prend en charge C++ 0x fonctions lambda, vous pouvez simplifier le code en éliminant la définition de foncteur explicite:

main() { 
    std::vector<int> ui; 
    gen_sequence([&](int n)->void{ui.push_back(n);}); 
} 

(J'utilise toujours VS2008 donc je ne suis pas sûr J'ai la syntaxe lambda correcte)

Questions connexes