Je veux savoir pourquoi std::accumulate
(aka réduire) le 3ème paramètre est nécessaire. Pour ceux qui ne savent pas ce que accumulate
est, il est utilisé comme suit:
vector<int> V{1,2,3};
int sum = accumulate(V.begin(), V.end(), 0);
// sum == 6
L'appel à accumulate
équivaut à:
sum = 0; // 0 - value of 3rd param
for (auto x : V) sum += x;
Il existe également un quatrième paramètre facultatif, qui permet de remplacer l'addition par toute autre opération.
La justification que j'ai entendue est que si vous avez besoin, disons de ne pas additionner, mais de multiplier les éléments d'un vecteur, nous avons besoin d'une autre valeur initiale (différente de zéro):
vector<int> V{1,2,3};
int product = accumulate(V.begin(), V.end(), 1, multiplies<int>());
Mais pourquoi ne pas faire comme Python - définissez la valeur initiale de V.begin()
et utilisez la plage à partir de V.begin()+1
. Quelque chose comme ça:
int sum = accumulate(V.begin()+1, V.end(), V.begin());
Cela fonctionnera pour n'importe quel op. Pourquoi le 3ème paramètre est-il nécessaire?
En l'état actuel des choses, c'est agaçant pour un code qui sait à coup sûr qu'une plage n'est pas vide et qui souhaite commencer à accumuler à partir du premier élément de la plage. En fonction de l'opération utilisée pour accumuler, la valeur «zéro» à utiliser n'est pas toujours évidente.
Si, par contre, vous ne fournissez qu'une version nécessitant des plages non vides, cela peut gêner les appelants qui ne savent pas si leurs plages ne sont pas vides. Un fardeau supplémentaire leur est imposé.
L'un des points de vue est que le meilleur des deux mondes est bien sûr de fournir les deux fonctionnalités. Par exemple, Haskell fournit à la fois foldl1
et foldr1
(qui nécessite des listes non vides), aux côtés de foldl
et foldr
(miroir de std::transform
).
Une autre perspective est que, puisque l’une peut être mise en œuvre de manière triviale (comme vous l’avez montré: std::transform(std::next(b), e, *b, f)
- std::next
est en C++ 11 mais le point est toujours valable), il est préférable de définir l’interface de la manière suivante: aussi minime que cela puisse être sans réelle perte de pouvoir expressif.
Si vous vouliez accumulate(V.begin()+1, V.end(), V.begin())
, vous pouvez simplement écrire cela. Mais que se passe-t-il si vous pensiez que v.begin () pourrait être v.end () (c.-à-d. V est vide)? Que se passe-t-il si v.begin() + 1
n'est pas implémenté (car v implémente uniquement ++, pas l'ajout généralisé)? Que se passe-t-il si le type de l'accumulateur n'est pas le type des éléments? Par exemple.
std::accumulate(v.begin(), v.end(), 0, [](long count, char c){
return isalpha(c) ? count + 1 : count
});
Parce que les algorithmes de bibliothèque standard sont supposés fonctionner pour des plages arbitraires d'itérateurs (compatibles). Ainsi, le premier argument de accumulate
ne doit pas nécessairement être begin()
, il peut s'agir de tout itérateur situé entre begin()
et un avant end()
. Il pourrait également utiliser des itérateurs inversés.
L'idée est de dissocier les algorithmes des données. Votre suggestion, si je comprends bien, nécessite une certaine structure dans les données.
Ce n'est en effet pas nécessaire. Notre base de code a des surcharges de 2 et 3 arguments qui utilisent une valeur T{}
.
Cependant, std::accumulate
est assez vieux; ça vient de la STL originale. Notre base de code a une logique sophistiquée std::enable_if
pour distinguer "2 itérateurs et valeur initiale" de "2 itérateurs et opérateur de réduction". Cela nécessite C++ 11. Notre code utilise également un type de retour final (auto accumulate(...) -> ...
) pour calculer le type de retour, une autre fonctionnalité C++ 11.