web-dev-qa-db-fra.com

Quel est le problème avec le passage de l'itérateur C ++ par référence?

J'ai écrit quelques fonctions avec un prototype comme celui-ci:

template <typename input_iterator>
int parse_integer(input_iterator &begin, input_iterator end);

L'idée est que l'appelant fournirait une plage de caractères, et la fonction interpréterait les caractères comme une valeur entière et la renverrait, en laissant begin à une heure après le dernier caractère utilisé. Par exemple:

std::string sample_text("123 foo bar");
std::string::const_iterator p(sample_text.begin());
std::string::const_iterator end(sample_text.end());
int i = parse_integer(p, end);

Cela laisserait i défini sur 123 et p "pointant" vers l'espace avant foo.

On m'a depuis dit (sans explication) que c'était une mauvaise forme de passer un itérateur par référence. Est-ce une mauvaise forme? Si oui, pourquoi?

40
Adrian McCarthy

Il n'y a rien de vraiment mauvais, mais cela limitera certainement l'utilisation du modèle. Vous ne pourrez pas simplement mettre un itérateur retourné par quelque chose d'autre ou généré comme v.begin(), car ceux-ci seront temporaires. Vous devrez toujours d'abord faire une copie locale, ce qui est une sorte de passe-partout pas vraiment agréable à avoir.

Une façon consiste à le surcharger:

int parse_integer(input_iterator begin, input_iterator end, 
                  input_iterator &newbegin);

template<typename input_iterator>
int parse_integer(input_iterator begin, input_iterator end) {
    return parse_integer(begin, end, begin);
} 

Une autre option consiste à avoir un itérateur de sortie dans lequel le nombre sera écrit:

template<typename input_iterator, typename output_iterator>
input_iterator parse_integer(input_iterator begin, input_iterator end,
                             output_iterator out);

Vous aurez la valeur de retour pour renvoyer le nouvel itérateur d'entrée. Et vous pouvez ensuite utiliser un itérateur d'insertion pour mettre les nombres analysés dans un vecteur ou un pointeur pour les mettre directement dans un entier ou un tableau de ceux-ci si vous connaissez déjà la quantité de nombres.

int i;
b = parse_integer(b, end, &i);

std::vector<int> numbers;
b = parse_integer(b, end, std::back_inserter(numbers));

En général:

Si vous passez une référence nonconst, l'appelant ne sait pas si l'itérateur est en cours de modification.

Vous pouvez passer une référence const, mais généralement les itérateurs sont suffisamment petits pour ne donner aucun avantage sur le passage par valeur.

Dans votre cas:

Je ne pense pas qu'il y ait quelque chose de mal avec ce que vous faites, sauf que ce n'est pas trop standard en ce qui concerne l'utilisation de l'itérateur.

4
Reunanen

À mon avis, si vous voulez faire cela, l'argument devrait être un pointeur vers l'itérateur que vous changerez. Je ne suis pas un grand fan des arguments de référence non const car ils masquent le fait que le paramètre passé pourrait changer. Je sais qu'il y a beaucoup d'utilisateurs C++ qui ne sont pas d'accord avec mon opinion à ce sujet - et ça va.

Cependant, dans ce cas, il est donc commun pour que les itérateurs soient traités comme des arguments de valeur que je pense que c'est une très mauvaise idée de passer des itérateurs par référence non const et modifier l'itérateur passé. Cela va à l'encontre de la façon dont les itérateurs sont généralement utilisés.

Puisqu'il existe un excellent moyen de faire ce que vous voulez qui n'a pas ce problème, je pense que vous devriez l'utiliser:

template <typename input_iterator>
int parse_integer(input_iterator* begin, input_iterator end);

Maintenant, un appelant devrait faire:

int i = parse_integer(&p, end);

Et il sera évident que l'itérateur peut être changé.

Au fait, j'aime aussi suggestion de litb de retourner le nouvel itérateur et de placer les valeurs analysées dans un emplacement spécifié par un itérateur de sortie.

2
Michael Burr

Dans ce contexte, je pense que le passage d'un itérateur par référence est parfaitement sensé, tant qu'il est bien documenté.

Il convient de noter que votre approche (passer un itérateur par référence pour garder une trace de l'endroit où vous vous trouvez lors de la création de jetons d'un flux) est exactement l'approche adoptée par boost :: tokenizer . Voir notamment la définition du TokenizerFunction Concept . Dans l'ensemble, je trouve que boost :: tokenizer est assez bien conçu et bien pensé.

2
Edward Loper

Quand ils disent "ne passez pas par référence", c'est peut-être parce qu'il est plus normal/idiomatique de passer des itérateurs en tant que paramètres de valeur, au lieu de les passer par référence const: ce que vous avez fait, pour le deuxième paramètre.

Dans cet exemple, cependant, vous devez renvoyer deux valeurs: la valeur int analysée et la valeur de l'itérateur nouveau/modifié; et étant donné qu'une fonction ne peut pas avoir deux codes de retour, le codage de l'un des codes de retour en tant que référence non constante est IMO normal.

Une alternative serait de le coder quelque chose comme ceci:

//Comment: the return code is a pair of values, i.e. the parsed int and etc ...
pair<int, input_iterator> parse(input_iterator start, input_iterator end)
{
}
2
ChrisW

Je pense que les algorithmes de la bibliothèque standard transmettent les itérateurs par valeur exclusivement (quelqu'un va maintenant publier une exception évidente à cela) - cela peut être l'origine de l'idée. Bien sûr, rien ne dit que votre propre code doit ressembler à la bibliothèque standard!

1
anon