17/10/2017
I C++'s standardbibliotek er std::list en yderst nyttig container, der er implementeret som en dobbelt-linket liste. Dette giver den unikke fordele, såsom hurtig indsættelse og sletning af elementer hvor som helst i listen. Men mange udviklere, især dem der kommer fra at bruge std::vector eller almindelige arrays, undrer sig over en bestemt udeladelse: Hvorfor har std::list ikke en indekseringsoperator (operator[])? Svaret ligger i et grundlæggende designprincip, der prioriterer ydeevne og forhindrer vildledende kode. At forstå disse designvalg er afgørende for at skrive effektiv og robust C++ kode.

Hvorfor findes `operator[]` ikke for `std::list`?
Kernen i problemet er den underliggende datastruktur. En std::list er en linket liste, hvilket betyder, at elementerne ikke er gemt i en sammenhængende hukommelsesblok som i en std::vector. I stedet indeholder hvert element en værdi samt et eller flere links (pegepinde) til det næste og/eller forrige element i sekvensen. For at finde det 'n'-te element i en linket liste, er der ingen anden måde end at starte fra begyndelsen (eller slutningen) og følge linkene et ad gangen, indtil man når den ønskede position.
Denne proces kaldes en traversering, og dens tidskompleksitet er lineær, eller O(n). Det betyder, at den tid, det tager at finde et element, vokser proportionalt med listens størrelse. Hvis en liste har 1.000.000 elementer, vil det i gennemsnit kræve 500.000 hop at finde et element i midten.
ISO C++ standarden specificerer (i 23.1.1[lib.sequence.reqmts]/12), at alle STL-sekvenser, der understøtter operator[], skal gøre det i amortiseret konstant tid, altså O(1). Dette er opnåeligt for std::vector og std::deque, da de bruger sammenhængende hukommelse og kan beregne adressen på ethvert element med en simpel aritmetisk operation. Da std::list ikke kan opfylde dette krav, blev det besluttet at udelade operatoren helt. At inkludere den ville være vildledende og kunne lokke udviklere til at skrive meget ineffektiv kode. Forestil dig følgende løkke:
// ADVARSEL: Pseudokode, der viser et O(n^2) problem std::list<int> min_liste; // ... listen fyldes med N elementer ... for (int i = 0; i < min_liste.size(); ++i) { int x = min_liste[i]; // Hver af disse operationer er O(n) // ... gør noget med x ... }Denne løkke ser uskyldig ud, men den har en katastrofal ydeevne med en kompleksitet på O(n²). For hver iteration i løkken (n iterationer) skal den traversere listen fra starten for at finde elementet ved indeks i (en O(n) operation). Resultatet er n * n = n² operationer, hvilket gør koden ekstremt langsom for store lister.
Det korrekte alternativ: Iteratorer og `std::advance`
Den korrekte måde at arbejde med elementer i en std::list er ved hjælp af iteratorer. Hvis du har brug for at nå et element med et bestemt offset fra en kendt position, kan du bruge algoritmen std::advance:
#include <list> #include <iterator> std::list<int> min_liste = {10, 20, 30, 40, 50}; auto it = min_liste.begin(); // Iterator peger på det første element (10) // Flyt iteratoren 3 positioner frem std::advance(it, 3); // Nu peger 'it' på det fjerde element (40) int vaerdi = *it;Selvom std::advance også har en O(n) kompleksitet for std::list, gør dens navn og brug det klart, at der sker en potentielt dyr operation, i modsætning til den vildledende enkelhed af [].
Relationelle Operatorer for `std::list`
Mens indeksering er udeladt, er std::list fuldt udstyret med overbelastede relationelle operatorer, som gør det nemt at sammenligne to lister. Disse operatorer er defineret i <list> headeren og omfatter:
operator==(lig med)operator!=(ikke lig med)operator<(mindre end)operator<=(mindre end eller lig med)operator>(større end)operator>=(større end eller lig med)
Disse operatorer giver en intuitiv måde at sammenligne indholdet af to lister på. Lad os se nærmere på, hvordan de fungerer.
Sådan fungerer sammenligningerne
Ligestillingsoperatorer (`==` og `!=`)
Når du sammenligner to lister med operator==, udføres der to trin:
- Først sammenlignes størrelsen på de to lister (
lhs.size() == rhs.size()). Hvis størrelserne er forskellige, er listerne ikke ens, og funktionen returnererfalsemed det samme. Dette er en meget hurtig O(1) operation. - Hvis størrelserne er ens, fortsætter funktionen med at sammenligne elementerne parvis, fra start til slut, ved hjælp af
operator==for elementtypen. Hvis der findes et par elementer, der ikke er ens, returneresfalse. Hvis alle elementer er ens, returnerestrue. Denne del har en lineær kompleksitet, O(n).
Operatoren operator!= er simpelthen defineret som det modsatte af operator== (!(a == b)).
Ordensoperatorer (`<`, `<=`, `>`, `>=`)
Disse operatorer udfører en leksikografisk sammenligning, som er den samme type sammenligning, der bruges til at sortere ord i en ordbog.

Når du bruger operator<, sammenlignes elementerne fra de to lister parvis. Ved det første par elementer, hvor lhs-elementet er mindre end rhs-elementet, konkluderes det, at lhs er mindre end rhs, og true returneres. Hvis rhs-elementet er mindre, konkluderes det, at lhs ikke er mindre end rhs, og false returneres. Hvis listerne er identiske op til slutningen af den ene liste, men den ene er kortere, betragtes den kortere liste som værende mindre. Denne proces har også en lineær kompleksitet, O(n), i værste fald.
De andre ordensoperatorer er defineret ud fra < og ==:
| Operation | Ækvivalent operation |
|---|---|
a > b | b < a |
a <= b | !(b < a) |
a >= b | !(a < b) |
Kompleksitetsoversigt
Det er vigtigt for enhver C++ udvikler at have en klar forståelse af ydeevnen af de operationer, de bruger. Her er en oversigt over tidskompleksiteten for std::list's relationelle operatorer.
| Operator | Betingelse | Kompleksitet |
|---|---|---|
==, != | Listerne har forskellig størrelse | Konstant - O(1) |
==, != | Listerne har samme størrelse | Op til lineær - O(n) |
<, <=, >, >= | Generel sammenligning | Op til lineær - O(n) |
Ofte Stillede Spørgsmål (FAQ)
Sp: Hvad betyder leksikografisk sammenligning helt præcist?
Sv: Forestil dig at sammenligne to ord, f.eks. "bog" og "bil". Du starter med det første bogstav: 'b' er lig med 'b'. Du går videre til det næste: 'o' kommer efter 'i' i alfabetet. Derfor er "bog" større end "bil". Leksikografisk sammenligning for std::list fungerer på præcis samme måde, men med listeelementerne i stedet for bogstaver. Den første position, hvor elementerne er forskellige, afgør resultatet.
Sp: Er disse sammenligninger trådsikre?
Sv: Nej. Under en sammenligning tilgås begge lister. Hvis en anden tråd modificerer en af listerne, mens sammenligningen er i gang, kan det føre til udefineret adfærd (data race). Selve sammenligningsoperationerne modificerer dog ikke listerne.
Sp: Hvad sker der, hvis elementtypen ikke understøtter `==` eller `<`?
Sv: Koden vil ikke kompilere. For at kunne bruge de relationelle operatorer på std::list, skal typen T selv understøtte de nødvendige operatorer (operator== for lighed og operator< for ordenssammenligninger).
Konklusion
Valget om at udelade operator[] fra std::list er en bevidst og gennemtænkt beslutning fra C++ standardkomiteens side. Det beskytter udviklere mod at skrive utilsigtet langsom kode og tvinger dem til at bruge den korrekte og mere udtryksfulde metode med iteratorer. Samtidig giver de overbelastede relationelle operatorer en kraftfuld og intuitiv måde at sammenligne hele lister på, med en ydeevne, der er optimeret til de mest almindelige scenarier. At forstå disse nuancer er nøglen til at mestre C++'s standardbibliotek og skrive kode, der ikke kun er korrekt, men også effektiv.
Hvis du vil læse andre artikler, der ligner Forstå std::list operatorer i C++, kan du besøge kategorien Teknologi.
