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

197

Chaque conteneur standard a une méthode beginet endpour retourner des itérateurs pour ce conteneur. Cependant, C ++ 11 a apparemment introduit des fonctions libres appelées std::beginet std::endqui appellent les fonctions membres beginet end. Donc, au lieu d'écrire

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

tu écrirais

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

Dans son exposé, Writing Modern C ++ , Herb Sutter dit que vous devez toujours utiliser les fonctions gratuites maintenant lorsque vous voulez l'itérateur de début ou de fin pour un conteneur. Cependant, il n'entre pas dans les détails de pourquoi vous voudriez. En regardant le code, il vous enregistre tous d'un caractère. Ainsi, en ce qui concerne les conteneurs standard, les fonctions gratuites semblent totalement inutiles. Herb Sutter a indiqué qu'il y avait des avantages pour les conteneurs non standard, mais encore une fois, il n'est pas entré dans les détails.

Donc, la question est de savoir ce que font exactement les versions de fonction libres de std::begin et std::endfont au-delà d'appeler leurs versions de fonctions membres correspondantes, et pourquoi voudriez-vous les utiliser?

Jonathan M Davis
la source
29
C'est un personnage de moins, gardez
HostileFork dit de ne pas faire confiance au SE
Je détesterais en quelque sorte de les utiliser parce que je devrais répéter std::tout le temps.
Michael Chourdakis

Réponses:

162

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

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.

Matthieu M.
la source
7
@JonathanMDavis: vous pouvez avoir le endpour les tableaux statiquement déclarés ( int foo[5]) en utilisant des astuces de programmation de modèle. Une fois qu'il s'est désintégré en un pointeur, vous n'avez bien sûr pas de chance.
Matthieu M.
33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh
6
@JonathanMDavis: Comme les autres l'ont indiqué, il est certainement possible d'obtenir beginet endsur un tableau C tant que vous ne l'avez pas déjà décomposé en pointeur - @Huw le précise. Quant à savoir pourquoi vous voudriez: imaginez que vous avez refactorisé le code qui utilisait un tableau pour utiliser un vecteur (ou vice versa, pour une raison quelconque). Si vous avez utilisé beginet end, et peut-être des effets de frappe intelligents, le code d'implémentation n'aura pas à changer du tout (sauf peut-être certains des typedefs).
Karl Knechtel
31
@JonathanMDavis: Les tableaux ne sont pas des pointeurs. Et pour tout le monde: Pour mettre fin à cette confusion toujours plus importante, arrêtez de faire référence à (certains) pointeurs comme des "tableaux pourris". Il n'y a pas une telle terminologie dans le langage, et il n'y a vraiment aucune utilité. Les pointeurs sont des pointeurs, les tableaux sont des tableaux. Les tableaux peuvent être convertis en un pointeur vers leur premier élément implicitement, mais l'est toujours juste un ancien pointeur normal, sans distinction avec les autres. Bien sûr, vous ne pouvez pas obtenir la «fin» d'un pointeur, le cas est fermé.
GManNickG
5
Eh bien, à part les tableaux, il existe un grand nombre d'API qui exposent les aspects de type conteneur. De toute évidence, vous ne pouvez pas modifier une API tierce, mais vous pouvez facilement écrire ces fonctions de début / fin autonomes.
edA-qa mort-ora-y
35

Considérez le cas lorsque vous avez une bibliothèque contenant une classe:

class SpecialArray;

il a 2 méthodes:

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

pour itérer sur ses valeurs que vous devez hériter de cette classe et définir begin()et les end()méthodes 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());
}

SpecialArrayIteratorest 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 iet epeut être légalement utilisé pour l'itération et l'accès aux valeurs de SpecialArray

GreenScape
la source
8
Cela ne devrait pas inclure les template<>lignes. Vous déclarez une nouvelle surcharge de fonction, sans spécialiser un modèle.
David Stone
33

L'utilisation des fonctions beginet endfree ajoute une couche d'indirection. Habituellement, cela est fait pour permettre plus de flexibilité.

Dans ce cas, je peux penser à quelques utilisations.

L'utilisation la plus évidente est pour les tableaux C (pas les pointeurs c).

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

En aparté, le prochain C de la tour devrait copier de D' notation pseudo-membre . Si a.foo(b,c,d)n'est pas défini, il essaie à la place foo(a,b,c,d). C'est juste un peu de sucre syntaxique pour nous aider les pauvres humains qui préfèrent le sujet puis l'ordre des verbes.

deft_code
la source
5
La notation pseudo-membre ressemble aux méthodes d'extension C # /. Net . Ils sont utiles dans diverses situations, mais comme toutes les fonctionnalités, ils peuvent être sujets à des «abus».
Gareth Wilson
5
La notation pseudo-membre est une aubaine pour le codage avec Intellisense; frapper "a". affiche les verbes pertinents, libérant la puissance du cerveau de la mémorisation des listes et aidant à découvrir les fonctions API pertinentes peut aider à empêcher la duplication des fonctionnalités, sans avoir à enchaîner les fonctions non membres dans les classes.
Matt Curtis
Il existe des propositions pour intégrer cela dans C ++, qui utilisent le terme de syntaxe d'appel de fonction unifiée (UFCS).
underscore_d
17

Pour répondre à votre question, les fonctions libres begin () et end () par défaut ne font rien de plus que d'appeler les fonctions membre .begin () et .end () du conteneur. De <iterator>, inclus automatiquement lorsque vous utilisez l' un des conteneurs standards comme <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 gratuites si elles ne font qu'appeler les fonctions membres de toute façon. Cela dépend vraiment du type d'objet vdans 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, ils font la même chose. Si votre objet vest 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 votre code pour les tableaux T = C, les chaînes C, les énumérations, etc. En utilisant les fonctions non membres, vous annoncez 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 désormais avec les tableaux T = C et les chaînes C. Écrivant maintenant une petite quantité de code d'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 rendre votre code compatible avec les énumérations itérables. Je pense que le principal point de Herb est que l'utilisation des fonctions gratuites est tout aussi simple que l'utilisation des fonctions membres, et cela donne à votre code une compatibilité descendante avec les types de séquence C et une compatibilité ascendante avec les types de séquence non stl (et les types future-stl!), à faible coût pour les autres développeurs.

Nate
la source
Bons exemples. Je ne prendrais pas un enumou tout autre type fondamental par référence, cependant; ils seront moins chers à copier qu'à indirectement.
underscore_d
6

Un avantage de std::beginetstd::end est qu'ils servent de points d'extension pour implémenter une interface standard pour les classes externes.

Si vous souhaitez utiliser une CustomContainerclasse avec une fonction de boucle ou de modèle basée sur une plage qui attend .begin()et des .end()méthodes, vous devrez évidemment implémenter ces méthodes.

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

Cela n'est pas toujours possible, par exemple lors de l'utilisation d'une bibliothèque externe, en particulier une source commerciale et fermée.

Dans de telles situations, std::beginet std::endutile, car on peut fournir une API d'itérateur sans modifier la classe elle-même, mais plutôt surcharger les fonctions libres.

Exemple: supposons que vous souhaitiez implémenter une count_iffonction 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, pour n'importe quelle classe que vous souhaitez utiliser avec cette coutume count_if, vous n'avez qu'à ajouter deux fonctions gratuites, au lieu de modifier ces classes.

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

En bref, ADL signifie que lorsqu'un compilateur résout une fonction non qualifiée (c'est-à-dire une fonction sans espace de noms, comme beginau 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::beginet some_lib::end - puisque CustomContainerc'est some_lib::aussi le cas, le compilateur utilisera ces surcharges danscount_if .

C'est aussi la raison d'avoir using std::begin;et using std::end;de participer count_if. Cela nous permet d'utiliser sans réserve beginet end, par conséquent, de permettre ADL et de permettre au compilateur de choisir std::beginet std::endquand aucune autre alternative n'est trouvée.

Nous pouvons manger le cookie et l'avoir - c'est-à-dire avoir un moyen de fournir une implémentation personnalisée de begin/end tandis que le compilateur peut revenir à ceux standard.

Quelques notes:

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

  • Comme d'autres réponses le mentionnent, les std::versions ont 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.

  • L'utilisation de std::beginet d'amis est particulièrement bonne lors de l'écriture de code de modèle, car cela rend ces modèles plus génériques. Pour les non-modèles, vous pourriez tout aussi bien utiliser des méthodes, le cas échéant.

PS Je sais que ce post a presque 7 ans. Je suis tombé dessus parce que je voulais répondre à une question marquée comme doublon et j'ai découvert qu'aucune réponse ne mentionne ADL.

joe_chip
la source
Bonne réponse, en particulier en expliquant ADL ouvertement, plutôt que de laisser cela à l'imagination comme tout le monde l'a fait - même quand ils le montraient en action!
underscore_d
5

Alors que les fonctions non membres n'offrent aucun avantage aux conteneurs standard, leur utilisation renforce un style plus cohérent et plus flexible. Si vous souhaitez à un moment donné étendre une classe de conteneur non std existante, vous préférez définir des 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 toujours en utilisant les fonctions gratuites rend votre code plus flexible en ce que vous pouvez remplacer le conteneur std par un conteneur non std plus facilement et le type de conteneur sous-jacent est plus transparent pour votre code car il prend en charge une plus grande variété d'implémentations de conteneurs.

Mais bien sûr, cela doit toujours être pondéré correctement et la surabstraction n'est pas bonne non plus. Bien que l'utilisation des fonctions gratuites ne soit pas une sur-abstraction, elle rompt néanmoins la compatibilité avec le code C ++ 03, qui à ce jeune âge de C ++ 11 pourrait toujours être un problème pour vous.

Christian Rau
la source
3
En C ++ 03, vous pouvez simplement utiliser boost::begin()/end() , donc il n'y a pas de réelle incompatibilité :)
Marc Mutz - mmutz
1
@ MarcMutz-mmutz Eh bien, la dépendance de boost n'est pas toujours une option (et est tout à fait exagérée si elle n'est utilisée que pour begin/end). Je considérerais donc cela également comme une incompatibilité avec le C ++ 03 pur. Mais comme dit, il s'agit d'une incompatibilité plutôt petite (et qui diminue), car C ++ 11 (au moins begin/enden particulier) est de plus en plus adopté, de toute façon.
Christian Rau
0

En fin de compte, l'avantage est dans le code qui est généralisé de sorte qu'il soit indépendant du conteneur. Il peut fonctionner sur un std::vector, un tableau ou une plage sans modification du code lui-même.

De plus, des conteneurs, même des conteneurs n'appartenant pas à un propriétaire, peuvent être mis à niveau de telle sorte qu'ils peuvent également être utilisés de manière agnostique par code à l'aide d'accesseurs basés sur des plages non membres.

Voir ici pour plus de détails.

Jonathan Mee
la source