web-dev-qa-db-fra.com

Pourquoi utiliser des fonctions de début et de fin non membres en C ++ 11?

Chaque conteneur standard a une méthode begin et end pour renvoyer des itérateurs pour ce conteneur. Cependant, C++ 11 a apparemment introduit des fonctions libres appelées std::begin et std::end qui appelle les fonctions membres begin et end. Donc, au lieu d'écrire

auto i = v.begin();
auto e = v.end();

tu écrirais

using std::begin;
using std::end;
auto i = begin(v);
auto e = end(v);

Dans son exposé, Writing Modern C++ , Herb Sutter indique que vous devez toujours utiliser les fonctions libres maintenant lorsque vous voulez le début ou la fin de l'itérateur d'un conteneur. Cependant, il n'entre pas dans les détails pour pourquoi vous voudriez. En regardant le code, vous enregistrez tous un caractère. Ainsi, dans la mesure où les conteneurs standard disparaissent, les fonctions libres semblent être complètement inutiles. Herb Sutter a indiqué que les conteneurs non standard présentaient des avantages, mais encore une fois, il n'a pas détaillé les détails.

La question est donc de savoir ce que font exactement les versions à fonctions libres de std::begin et std::end fais plus que d’appeler les versions des fonctions membres correspondantes et pourquoi voudriez-vous les utiliser?

184
Jonathan M Davis

Comment appelez-vous .begin() et .end() sur un tableau C?

Les fonctions libres permettent une programmation plus générique car elles peuvent être ajoutées par la suite, sur une structure de données que vous ne pouvez pas modifier.

154
Matthieu M.

Prenons le cas où vous avez une bibliothèque contenant la classe:

class SpecialArray;

il a 2 méthodes:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

pour parcourir ses valeurs, vous devez hériter de cette classe et définir les méthodes begin() et end() pour les cas où

auto i = v.begin();
auto e = v.end();

Mais si vous utilisez toujours

auto i = begin(v);
auto e = end(v);

tu peux le faire:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

SpecialArrayIterator est quelque chose comme:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

maintenant i et e peuvent être utilisés légalement pour l'itération et l'accès aux valeurs de SpecialArray

34
GreenScape

L'utilisation des fonctions begin et end free ajoute une couche d'indirection. Cela est généralement fait pour permettre plus de flexibilité.

Dans ce cas, je peux penser à quelques utilisations.

L'utilisation la plus évidente concerne les tableaux C (et non les pointeurs C).

Une autre solution consiste à essayer d'utiliser un algorithme standard sur un conteneur non conforme (c'est-à-dire qu'il manque une méthode .begin() au conteneur). En supposant que vous ne puissiez pas réparer le conteneur, la meilleure option consiste à surcharger la fonction begin. Herb suggère que vous utilisiez toujours la fonction begin pour promouvoir l'uniformité et la cohérence de votre code. Au lieu de devoir rappeler quels conteneurs supportent la méthode begin et lesquels ont besoin de la fonction begin.

En passant, le prochain rev C++ devrait copier les D notation pseudo-membre. Si a.foo(b,c,d) n'est pas défini, il essaie plutôt foo(a,b,c,d). C’est juste un petit sucre syntaxique pour nous aider, pauvres humains, qui préférons le sujet à l’ordre des verbes.

31
deft_code

Pour répondre à votre question, les fonctions libres begin () et fin () ne font, par défaut, rien de plus que d'appeler les fonctions .begin () et .end () du membre du conteneur. De <iterator>, inclus automatiquement lorsque vous utilisez l’un des conteneurs standard tels que <vector>, <list>, etc., vous obtenez:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

La deuxième partie de votre question est de savoir pourquoi préférer les fonctions libres si elles ne font que téléphoner aux fonctions membres. Cela dépend vraiment du type d'objet v dans votre exemple de code. Si le type de v est un type de conteneur standard, comme vector<T> v; alors peu importe si vous utilisez les fonctions libres ou membres, elles font la même chose. Si votre objet v est plus générique, comme dans le code suivant:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Ensuite, l’utilisation des fonctions membres rompt le code pour les tableaux T = C, les chaînes C, les enums, etc. En utilisant les fonctions non membres, vous publiez une interface plus générique que les utilisateurs peuvent facilement étendre. En utilisant l'interface de fonction gratuite:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Le code fonctionne maintenant avec les tableaux T = C et les chaînes C. Ecrivez maintenant une petite quantité de code adaptateur:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Nous pouvons également obtenir que votre code soit compatible avec les énumérations itérables. Je pense que l'argument principal de Herb est que l'utilisation des fonctions libres est aussi simple que l'utilisation des fonctions membres et donne à votre code une compatibilité ascendante avec les types de séquence C et une compatibilité ascendante avec les types de séquence non stl (et les types à venir)! à faible coût pour les autres développeurs.

17
Nate

Alors que les fonctions non membres n'offrent aucun avantage aux conteneurs standard, leur utilisation impose un style plus cohérent et flexible. Si, à un moment donné, vous souhaitez étendre une classe de conteneur non std existante, préférez définir les surcharges des fonctions libres au lieu de modifier la définition de la classe existante. Donc, pour les conteneurs non-std, ils sont très utiles et le fait d'utiliser toujours les fonctions free rend votre code plus flexible en ce que vous pouvez remplacer le conteneur std par un conteneur non-std plus facilement et que le type de conteneur sous-jacent est plus transparent pour votre code. prend en charge une plus grande variété d'implémentations de conteneurs.

Mais bien sûr, il faut toujours bien peser cela et l'abstraction n'est pas bonne non plus. Bien que l'utilisation des fonctions libres ne soit pas vraiment une surabstraction, elle rompt néanmoins la compatibilité avec le code C++ 03, qui pourrait encore poser problème à ce jeune âge de C++ 11.

5
Christian Rau

Un avantage de std::begin Et de std::end Est qu’ils servent de points d’extension pour la mise en oeuvre d’une interface standard pour les classes externes.

Si vous souhaitez utiliser la classe CustomContainer avec une méthode de boucle ou de modèle basée sur la plage qui attend les méthodes .begin() et .end(), vous devez évidemment les implémenter. méthodes.

Si la classe fournit ces méthodes, ce n'est pas un problème. Si ce n'est pas le cas, vous devrez le modifier *.

Cela n’est pas toujours faisable, par exemple lors de l’utilisation de librairies externes, notamment commerciales et à sources fermées.

Dans de telles situations, std::begin Et std::end Sont pratiques, car on peut fournir une API itérateur sans modifier la classe elle-même, mais surcharger les fonctions libres.

Exemple: Supposons que vous souhaitiez implémenter la fonction count_if Qui prend un conteneur au lieu d'une paire d'itérateurs. Un tel code pourrait ressembler à ceci:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

Maintenant, quelle que soit la classe que vous souhaitez utiliser avec cette coutume count_if, Il vous suffit d'ajouter deux fonctions libres, au lieu de modifier ces classes.

Maintenant, C++ a un mécanisme appelé Argument Dependent Lookup (ADL), ce qui rend cette approche encore plus flexible.

En bref, ADL signifie que lorsqu'un compilateur résoudra une fonction non qualifiée (c'est-à-dire une fonction sans espace de noms, comme begin au lieu de std::begin), Il considérera également les fonctions déclarées dans les espaces de noms de ses arguments. Par exemple:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

Dans ce cas, peu importe que les noms qualifiés soient some_lib::begin Et some_lib::end - puisque CustomContainer est aussi dans some_lib::, Le compilateur utilisera également ces surcharges dans count_if.

C'est aussi la raison d'avoir using std::begin; Et using std::end; Dans count_if. Cela nous permet d’utiliser begin et end non qualifiés, autorisant ainsi ADL et à permettre au compilateur de choisir std::begin Et std::end Lorsqu'aucune autre alternative n'est trouvée.

Nous pouvons manger le cookie et avoir le cookie - i. e. avoir un moyen de fournir une implémentation personnalisée de begin/end tandis que le compilateur peut revenir aux standards.

Quelques notes:

  • Pour la même raison, il existe d'autres fonctions similaires: std::rbegin/rend, std::size Et std::data.

  • Comme d'autres réponses le mentionnent, les versions std:: Comportent des surcharges pour les tableaux nus. C'est utile, mais c'est simplement un cas particulier de ce que j'ai décrit ci-dessus.

  • Utiliser std::begin Et ses amis est une excellente idée pour écrire du code de modèle, car cela rend ces modèles plus génériques. Pour les non-modèles, vous pouvez tout aussi bien utiliser des méthodes, le cas échéant.

P. S. Je suis conscient que ce poste a presque 7 ans. Je suis tombé dessus parce que je voulais répondre à une question qui était marquée comme un doublon et j'ai découvert qu'aucune réponse ici ne mentionne ADL.

5
joe_chip