Sökresultat för

Iteratorer och STL Ranges

11 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
Iteratorer och STL Ranges

C++20 gav oss ranges som gör STL‑algoritmer både enklare och mer uttrycksfulla; skicka hela containern i stället för (begin, end), kedja transformerande views som en shell‑pipeline och stoppa iterationer med smarta sentinels. I artikeln visar jag hur egenbyggda containrar och iteratorer kan anpassas till ranges och views, inklusive transform, projektioner via medlemsfunktionspekare och hur pipelinen drivs.

I de olika kodsnuttarna nedan skapar jag en enkel-länkad lista med tal, kvadrerar dessa med en lambda och funktionen transform, samt lägger resultatet i en vector, som sedan skrivs ut. Detta gör jag för att illustrera de olika nyheterna i det som kallas ranges & views.

std::transform

Före C++20, när man vill applicera en STL funktion på elementen i en container, så anropade man den med ett intervall (begin, end).

auto sq     = [](auto x) { return x * x; };
auto input  = SingleLinkedList{1, 2, 3, 4, 5};
auto output = std::vector<int>{}; output.resize(input.size());
std::transform(input.begin(), input.end(), output.begin(), sq);
for (auto&& x: output) cout << x << " "; //1 4 9 16 25

std::ranges::transform

Med C++20, fick vi nya versioner av de flesta STL funktioner, i namespace std::ranges. Dels, kan man precis som tidigare skicka ett intervall. Men man kan också skicka med hela container objektet, som exemplet nedan visar.

auto input  = SingleLinkedList{1, 2, 3, 4, 5};
auto output = std::vector<int>{}; output.resize(input.size());
std::ranges::transform(input, output.begin(), sq);
for (auto&& x: output) cout << x << " "; //1 4 9 16 25

Sentinel Object

När man använder ett interval som input, kan man numera också skicka med en speciell slutmarkör, kallat sentinel object.

template<typename Iter, typename Value>
struct StopAfterValue {
    Iter   end;
    Value  cutoff;
    friend bool operator==(Iter it, StopAfterValue s) {
        return it == s.end || *it > s.cutoff;
    }
    friend bool operator==(StopAfterValue s, Iter it) { return it == s; }
    friend bool operator!=(Iter it, StopAfterValue s) { return !(it == s); }
    friend bool operator!=(StopAfterValue s, Iter it) { return !(it == s); }
};
//...
auto input  = SingleLinkedList{1, 2, 3, 4, 5, 6, 7, 8, 9};
auto output = std::vector<int>{}; output.resize(5);
auto stop   = StopAfterValue{.end = input.end(), .cutoff = 5};
std::ranges::transform(input.begin(), stop, output.begin(), sq);
for (auto&& x: output) cout << x << " "; //1 4 9 16 25

View-Pipeline

Utöver std::ranges har vi också ett namespace kallat std::views (alias till std::ranges::views). Här finns så kallade view-classes och view-factory-functions. Tankegången är att associera en container med en eller flera view objects. Riktigt spännande blir det när man skapar en pipeline via den överlagrade | operatorn, vilket får koden att likna shell pipes.

auto input  = SingleLinkedList{1, 2, 3, 4, 5};
auto output = input | std::views::transform(sq) | std::ranges::to<std::vector<int>>();
for (auto&& x: output) cout << x << " "; //1 4 9 16 25

Observera att std::ranges::to formellt tillhör C++23

Det sista exemplet kan förkortas genom att ta bort destinationen (sista delen) och låta for-each loop:en driva pipelinen.

auto input = SingleLinkedList{1, 2, 3, 4, 5};
for (auto&& x: input | std::views::transform(sq)) cout << x << " "; //1 4 9 16 25

En view-pipeline är en så kallad pull-pipeline, vilket innebär att sista delen måste vara drivande för att data ska börja flöda. En konsekvens är att det går att sätta samman en pipeline, utan drivande komponent i flera steg, och sen sist på lägga till en driver, såsom en loop eller ett destinationsobjekt.

Notera att std::views::transform inte skapar en ny container – elementen beräknas ‘on demand’ när loopen ber om nästa element.

Projection (member function pointer)

När man har en container med objekt, går det att extrahera delar av ett objekt med en så kallad projektion, vilket innebär att man skickar med en pekare till en medlemsfunktion, som blir anropad för att extrahera ett visst värde från respektive objekt. I exemplet nedan har vi en lista med Account-objekt och kvadrerar deras saldon.

struct Account {
    string accno{};
    double balance{};
    Account(string accno, double balance)
        : accno(std::move(accno)), balance(balance) {}
    auto get_accno()   const { return accno; }
    auto get_balance() const { return balance; }
};
//...
auto input = SingleLinkedList<Account>{
    {"AAA"s, 10}, {"BBB"s, 20}, {"CCC"s, 30}, {"DDD"s, 40}, {"EEE"s, 50}
};
auto output = input
              | std::views::transform(&Account::get_balance)
              | std::views::transform(sq)
              | std::ranges::to<std::vector<double>>();
for (auto&& x: output) cout << x << " "; //100 400 900 1600 2500

I standardalgoritmerna finns en särskild ‘projection’-parameter; här gör vi i stället motsvarande sak i två steg med views::transform.

Sammanfattning

  • C++20 Ranges låter dig köra STL-algoritmer mer uttrycksfullt: skicka hela containern istället för (begin, end), och kedja transformationer via views med | som en shell-pipeline.
  • Nya std::ranges-varianter av algoritmer (t.ex. ranges::transform) fungerar både med intervall och hela containrar, och kan stoppas med sentinels som markerar ett dynamiskt slut.
  • View-pipelines är “pull”-drivna: inget händer förrän sista steget konsumerar data (t.ex. for‑each eller to).
  • Projektioner gör det lätt att extrahera fält via medlemsfunktionspekare innan vidare transformation, t.ex. hämta saldo och kvadrera.
  • Egenbyggda containrar/iteratorer kan anpassas till ranges och views, så länge de exponerar rätt gränssnitt (begin/end, iterator-beteende) och uppfyller konceptkraven.

Kort sagt: ranges + views förenklar API:er, gör pipelines läsbara och flexibla, och spelar väl med både standardcontainrar och egna datatyper.