1. Einleitung
Um ehrlich zu sein, Programmierer verwandeln gern alles in Zahlen. Selbst wenn es eigentlich keine Zahlen sind. Sie haben eine Liste mit Noten — möchten die Summe. Eine Liste mit Aufgaben — möchten wissen, „wie viel Zeit insgesamt nötig ist“. Eine Liste mit Strings — möchten eine einzige Berichtzeile zusammensetzen. In all diesen Fällen ist die Logik ähnlich: Wir nehmen die Elemente der Reihe nach und akkumulieren ein gemeinsames Ergebnis.
Diese Idee heißt Faltung (reduction / fold). Man hat das oft manuell gemacht: eine Variable sum = 0 angelegt, eine Schleife durchlaufen und hinzugefügt. std::accumulate ist die standardmäßige, lesbare und ziemlich sichere Art, eine solche Operation ohne „rituelle“ Variablen und unnötigen Lärm auszudrücken. Es ist nicht magisch: im Inneren ist weiterhin eine normale Schleife, nur sauber verpackt.
Um accumulate nicht als Zauberspruch zu sehen, behalten Sie dieses sehr einfache Modell im Kopf:
flowchart LR
A[init] --> B{nächstes Element nehmen}
B --> C["Akkumulator aktualisieren: acc = op(acc, x)"]
C --> B
B --> D[Ende des Bereichs]
D --> E[acc zurückgeben]
2. Einbindung von <numeric> und Signatur von std::accumulate
Jetzt ein wichtiger praktischer Punkt: Viele Algorithmen leben in <algorithm>, aber std::accumulate steckt in einem anderen Header. Das ist einer der häufigsten Gründe für das „Warum kompiliert das nicht?!“ bei Einsteigern. Merken Sie sich also: accumulate befindet sich in <numeric>.
Die Basisform sieht so aus:
std::accumulate(first, last, init)
Sie läuft über den Bereich [first, last) und addiert die Elemente, beginnend mit init. Es gibt auch eine erweiterte Form mit Ihrer Operation:
std::accumulate(first, last, init, op)
Wobei op eine Funktion ist, die bestimmt, wie genau akkumuliert wird.
Mini‑Beispiel: Summe ganzer Zahlen
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4};
int sum = std::accumulate(v.begin(), v.end(), 0);
std::cout << sum << '\n'; // 10
}
Hier ist wichtig, dass 0 der Startwert ist. Wäre der Vektor leer, wäre das Ergebnis trotzdem 0 (und das ist ein sehr nützliches Verhalten).
Warum init den Ergebnistyp beeinflusst
Wenn Einsteiger zum ersten Mal hören „init beeinflusst den Typ“, reagieren viele mit „ach komm, das ist doch nur der Startwert“. Aber in C++ ist der Ergebnis- bzw. Akkumulatortyp tatsächlich an init gebunden, weil der Akkumulator genau mit diesem Wert beginnt. Starten Sie mit 0 (das ist ein int), dann wird sehr wahrscheinlich im int akkumuliert. Starten Sie mit 0LL, dann ist der Akkumulator ein long long.
Das ist besonders wichtig, wenn die Summe nicht in ein int passt. Und ja, das ist das typische Überlaufproblem, das im realen Code plötzlich zum Bug wird: „unser Budget ist negativ, sieht aber schick aus“.
Mini‑Beispiel: int-Überlauf vs. korrektes long long
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> prices{1'000'000'000, 1'000'000'000, 1'000'000'000};
long long total = std::accumulate(prices.begin(), prices.end(), 0LL);
std::cout << total << '\n'; // 3000000000
}
Hätten wir 0 statt 0LL geschrieben, würde in int akkumuliert und das Ergebnis könnte „kaputtgehen“ (plattform- und compilerabhängig). Daher die praktische Regel: wenn die Summe potenziell groß ist — starten Sie mit 0LL.
Spickzettel für den init
| Was wir berechnen | Typischer init | Warum |
|---|---|---|
| Summe von int | |
neutrales Element der Addition und Typ int |
| Größere Summe | |
neutrales Element, aber Typ long long |
| Summe double | |
damit in double akkumuliert wird |
| Produkt | |
neutrales Element der Multiplikation |
| String-Konkatenation | |
leerer String als neutrales Element |
3. Benutzerdefinierte Operation: nach Feld im struct akkumulieren
Bisher haben wir die „Standard“-Verhalten genutzt: accumulate addiert die Elemente. Aber eigentlich kann accumulate jede sinnvolle Aggregation, wenn Sie angeben, wie der Akkumulator aktualisiert wird. Für Einsteiger ist ein guter Zwischenschritt, nicht die Elemente selbst, sondern ein Feld innerhalb eines struct zu summieren.
Stellen Sie sich vor, wir haben Aufgaben mit einer geschätzten Zeit. Wir führen unser kleines Beispielprojekt „Mini‑Task‑Tracker“ fort.
Datenmodell
#include <string>
struct Task {
int id{};
std::string title;
int minutes{}; // geschätzte Zeit in Minuten
bool done{};
};
Nun wollen wir „wie viele Minuten haben alle Aufgaben zusammen“ berechnen. Ein Vektor von Aufgaben ist std::vector<Task>, und das Ergebnis ist eine Zahl. Die Operation muss also: den acc (Zahl) und ein Task nehmen und die aktualisierte Zahl zurückgeben.
Mini‑Beispiel: Minuten der Aufgaben per Funktion summieren
#include <numeric>
#include <vector>
int add_minutes(int acc, const Task& t) {
return acc + t.minutes;
}
Und die Verwendung:
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<Task> tasks{{1, "Read", 30, true}, {2, "Code", 50, false}};
int total = std::accumulate(tasks.begin(), tasks.end(), 0, add_minutes);
std::cout << total << '\n'; // 80
}
Beachten Sie die Parameterreihenfolge: Die Funktion für accumulate wird als op(accumulator, element) gedacht, also „aktualisiere den Akkumulator anhand des aktuellen Elements“.
4. Andere Aggregate: Produkt, Konkatenation, Zähler
Man kann leicht in der Vorstellung hängen bleiben „accumulate = Summe“. Besser ist zu denken: es ist ein Weg, eine Sequenz auf einen einzelnen Wert zu reduzieren. Manchmal ist das die Summe. Manchmal — das Produkt. Manchmal — eine Berichtzeile. Manchmal — sogar „zählen, wie viele erledigt sind“.
Produkt: neutrales Element — 1
Starten Sie das Produkt mit 0, und Sie bekommen für immer null (weil 0 * irgendwas = 0). Deshalb ist der Startwert 1.
#include <iostream>
#include <numeric>
#include <vector>
int mul(int acc, int x) {
return acc * x;
}
int main() {
std::vector<int> v{2, 3, 4};
int product = std::accumulate(v.begin(), v.end(), 1, mul);
std::cout << product << '\n'; // 24
}
String-Konkatenation: Akkumulieren von std::string
Ja, accumulate kann Strings zusammensetzen. Hier steckt aber eine Fallstricke: häufige Konkatenation kann teuer sein (Reallokationen). Wir gehen nicht tief in Optimierungen, aber ehrlich gesagt: für kleine Datenmengen ist es ok, für große sollte man nachdenken.
#include <iostream>
#include <numeric>
#include <string>
#include <vector>
std::string join_with_space(std::string acc, const std::string& s) {
if (!acc.empty()) acc += ' ';
acc += s;
return acc;
}
int main() {
std::vector<std::string> words{"C++", "is", "fine"};
std::string line = std::accumulate(words.begin(), words.end(), std::string{}, join_with_space);
std::cout << line << '\n'; // C++ is fine
}
Die Funktion nimmt acc per value, weil sie ihn sowieso ändert und zurückgibt. Für Einsteiger ist das ein guter Kompromiss in Bezug auf Verständlichkeit: „nimm den aktuellen Akkumulator, füge ein Stück hinzu, gib ihn zurück“.
Zähler für erledigte Aufgaben mit accumulate
Formal gibt es dafür count_if, das oft natürlicher aussieht. Als Übung zur Veranschaulichung der Faltung ist es aber ein ausgezeichnetes Beispiel: Ihr Akkumulator ist eine Zahl, das Element ist eine Aufgabe.
#include <numeric>
#include <vector>
int add_done_count(int acc, const Task& t) {
return acc + (t.done ? 1 : 0);
}
Verwendung:
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<Task> tasks{{1, "Read", 30, true}, {2, "Code", 50, false}, {3, "Sleep", 10, true}};
int done_cnt = std::accumulate(tasks.begin(), tasks.end(), 0, add_done_count);
std::cout << done_cnt << '\n'; // 2
}
5. accumulate im Mini‑Task‑App verwenden
Nun machen wir den praktischsten Schritt: Wir verwenden accumulate nicht nur als Demo, sondern als Teil der Anwendungslogik. Stellen Sie sich vor, wir haben einen Befehl „stats“, der eine kurze Statistik ausgibt: wie viele Aufgaben insgesamt, wie viele erledigt und wie viele Minuten insgesamt. Die Anzahl der Aufgaben ist tasks.size(), aber erledigte und Minuten sind Aggregationen.
Um main nicht zur langen Liste zu machen, kapseln wir die Aggregate in kleine Funktionen. Das entspricht der Idee eines „dünnen main“: leichter lesbar, einfacher visuell zu testen und leichter änderbar.
Mini‑Beispiel: Funktion „total_minutes“
#include <numeric>
#include <vector>
int total_minutes(const std::vector<Task>& tasks) {
return std::accumulate(tasks.begin(), tasks.end(), 0, add_minutes);
}
Mini‑Beispiel: Funktion „done_count“
#include <numeric>
#include <vector>
int done_count(const std::vector<Task>& tasks) {
return std::accumulate(tasks.begin(), tasks.end(), 0, add_done_count);
}
Mini‑Beispiel: Statistik ausgeben
#include <iostream>
#include <vector>
void print_stats(const std::vector<Task>& tasks) {
std::cout << "tasks: " << tasks.size() << '\n'; // tasks: 3
std::cout << "done: " << done_count(tasks) << '\n'; // done: 2
std::cout << "minutes total: " << total_minutes(tasks) << '\n'; // minutes total: 90
}
Mini‑Beispiel: kleines main, das stats zeigt
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks{{1, "Read", 30, true}, {2, "Code", 50, false}, {3, "Sleep", 10, true}};
print_stats(tasks);
}
Beachten Sie, wie main fast „deklarativ“ geworden ist: „hier sind die Daten, hier ist die Statistik“. Die Logik der Berechnungen lebt separat — das ist guter Stil.
Leerer Bereich: was zurückkommt und warum das praktisch ist
In realen Programmen gibt es oft leere Listen. Der Nutzer hat noch keine Aufgaben hinzugefügt, eine Datei ist noch nicht geladen, ein Filter hat alles weggefiltert. Hier zeigt sich eine sehr angenehme Eigenschaft: std::accumulate gibt bei leerem Bereich den init zurück.
Das bedeutet: wenn Sie das neutrale Element korrekt gewählt haben, erhalten Sie „sichere Mathematik“ ohne if (empty()) .... Beispielsweise ist die Summe einer leeren Liste 0, das Produkt einer leeren Liste 1, die Verkettung ist ein leerer String. Sie müssen keine überflüssigen Prüfungen schreiben, wenn das Verhalten bereits korrekt ist.
Für Einsteiger ist das ein Geschenk: weniger Verzweigungen — weniger Fehler.
6. Typische Fehler bei der Arbeit mit std::accumulate
Fehler Nr.1: vergisst #include von <numeric>.
Der Compiler kann dann so meckern, als hätten Sie eine nicht existente Funktion aufgerufen, obwohl Sie sicher sind, dass sie existiert. Die Lösung ist einfach: accumulate gehört zu <numeric>, nicht zu <algorithm>. Wenn Sie nur <algorithm> inkludiert haben, kann std::accumulate „nicht gefunden“ werden.
Fehler Nr.2: falscher init zerstört die Bedeutung des Ergebnisses.
Das klassische Beispiel ist ein Produkt, das mit 0 startet. Der Code kompiliert, das Programm läuft, aber das Ergebnis ist immer 0, und man beginnt, die Mathematik zu verdächtigen. Wichtig ist, Startwert und neutrales Element nicht zu verwechseln: für Multiplikation ist das neutrale Element 1, für Addition 0.
Fehler Nr.3: der Akkumulatortyp wird nicht kontrolliert und es gibt Überlauf.
Wenn Sie große Zahlen summieren und mit 0 (Typ int) starten, dann schließen Sie sich selbst in den Vertrag „rechne in int“ ein. Besser ist, mit 0LL zu starten, wenn das Ergebnis groß sein kann. Das ist keine „Optimierung“, das ist Korrektheit.
Fehler Nr.4: op hat falsche Argumentreihenfolge.
Manchmal schreibt man aus Gewohnheit op(element, acc), aber accumulate erwartet op(acc, element). Das kann zu seltsamen Kompilierfehlern oder seltsamer Logik führen, falls die Typen zufällig passen. Merken Sie: „erstes Argument — Akkumulator, zweites — aktuelles Element“.
Fehler Nr.5: versuchen, in op Seiteneffekte zu machen (Ausgabe, Container ändern).
Technisch kann man in op drucken, aber dann wird die Faltung zu einer „Schleife mit Effekten“, die schlechter lesbar und schwerer zu debuggen ist. Außerdem ist es besser, Faltungsoperationen als reine Funktionen zu denken: Eingabe → Ausgabe ohne Überraschungen. Wenn man drucken will — macht man das außerhalb, mit separatem Code.
GO TO FULL VERSION