# En djupdykning i C++ mest populära container

9 juli 2025 | https://www.ribomation.se/blog/2025/en-djupdykning-i-c++-mest-populara-container/

Det finns ett flertal containertyper i C++, men den absolut populäraste är `std::vector`, vilken är ämnet för denna artikel. Den består av ett dynamiskt allokerat minnesblock, som omallokeras allt efter behov, helt automatiskt.

```cpp
#include <print>
#include <vector>

int main() { 
    auto numbers = std::vector<int>{1, 2, 3, 4, 5}; 
    for (auto x : numbers)  std::print("{} ", x);
}
// prints: 1 2 3 4 5 
```

### Populering

Det finns flera olika sätt att populera en vektor. Här nedan visar jag det vanligaste varianterna:

```cpp
// compile-time initialization
auto numbers = std::vector<int>{1, 2, 3, 4, 5}; 
// [1, 2, 3, 4, 5]

// run-time initialization
auto numbers = std::vector<int>{};
for (auto k=1; k<=5; ++k) numbers.push_back(k);
// [1, 2, 3, 4, 5]

// based on another container
auto s = std::set<int>{5,3,1,4,5,1,1,3,2};
auto numbers = std::vector<int>{s.begin(), s.end()};
// [1, 2, 3, 4, 5]

// based on an array
int arr[] = {1,2,3,4,5};
auto numbers = std::vector<int>{std::begin(arr), std::end(arr)};
// [1, 2, 3, 4, 5]

// initialization with a count and a value
auto numbers = std::vector<int>(5, 42); 
// [42, 42, 42, 42, 42]

// initialization with a count and the default value of the element type
auto numbers = std::vector<int>(5); 
// [0, 0, 0, 0, 0]
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/rvv1f4naW)

I de två sista alternativen är det viktigt att använda parenteser och inte klamrar. Med klamrar i de två sista, initieras vektorn med två respektive ett värde. Det här är exempel på subtiliteter i språkets syntax, man dessvärre behöver ha koll på.

### CTAD

När man initierar med statiska värden, såsom det första alternativet visar, kan kompilatorn klura ut vilken elementtyp som används, utan att man behöver ange det själv. Det här kallas _Class Template Argument Deduction_, förkortat _CTAD_ och infördes i C++17.

```cpp
int main() {
    {
        auto numbers = std::vector{1,2,3,4,5};
        std::println("ints   : {}", numbers);
    }
    {
        auto numbers = std::vector{1.1,2.2,3.3,4.4,5.5};
        std::println("doubles: {}", numbers);
    }
    {
        using namespace std::string_literals;
        auto numbers = std::vector{"hej"s, "hopp"s, "tjabba"s};
        std::println("strings: {}", numbers);
    }
}
```

```shell
ints   : [1, 2, 3, 4, 5]
doubles: [1.1, 2.2, 3.3, 4.4, 5.5]
strings: ["hej", "hopp", "tjabba"]
```

### Anropa först `reserve()`

Låt oss se hur minneshanteringen fungerar när man använder `push_back`.

```cpp
auto operator new(size_t bytes) -> void* {
    std::println("[new] bytes={}", bytes);
    return ::malloc(bytes);
}
int main() {
    auto const N = 20;
    auto numbers = std::vector<int>{};
    for (auto k = 1; k <= N; ++k) numbers.push_back(k);
    std::println("w/o reserve: {}", numbers);
}
```

```shell
[new] bytes=4
[new] bytes=8
[new] bytes=16
[new] bytes=32
[new] bytes=64
[new] bytes=128
w/o reserve: [1, 2, 3, 4, 5, ...]
```

Här kan du tydligt se hur vektorn dubblerar sin interna array i takt med att värden sätts in. Det här är givetvis ett minnesslöseri, vilket motiverar att du skall alltid först anropa metoden `reserve()`, som förallokerar ett tillräckligt stort minnesblock.

```cpp
auto numbers = std::vector<int>{};
numbers.reserve(N);
for (auto k = 1; k <= N; ++k) numbers.push_back(k);
std::println("w reserve: {}", numbers);
```

```shell
[new] bytes=80
w reserve: [1, 2, 3, 4, 5, ...]
```

Denna gång allokerades bara ett minnesblock, med plats för 20 heltal om 4 bytes vardera. Om du kör detta exempel i Compiler Explorer, så ser du också att `std::println` allokerar minnesblock, om 85 respektive 83 bytes.

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/naYxs7eWc)

### Sätt inte in element i början

Vektorn är konstruerad för insättning i slutet, men inte för insättning i början. Så det finns metoden `push_back()` men inte `push_front()`. Emellertid, finns metoden `insert()` som kan sätta in ett element på godtycklig plats i en vektor. Låt oss kika på hur det går till.

```cpp
struct Dummy {
    inline static int next = 1;
    int value = next++;
    Dummy() { std::println("Dummy({})", value); }
    Dummy(Dummy const& rhs) { 
        std::println("Dummy(Dummy const& {})", rhs.value); 
    }
};

int main() {
    std::println("-- initialization of 3 elements --");
    auto v = std::vector<Dummy>(3);
    
    std::println("-- insertion at front --");
    auto x = Dummy{};
    v.insert(v.begin(), x);
    for (auto& d : v) std::print("{} ", d.value);
}
```

```shell
-- initialization of 3 elements --
Dummy(1)
Dummy(2)
Dummy(3)
-- insertion at front --
Dummy(4)
Dummy(Dummy const& 4)
Dummy(Dummy const& 1)
Dummy(Dummy const& 2)
Dummy(Dummy const& 3)
4 1 2 3 
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/svsjKnMz4)

Så, vad händer här egentligen? Jo, de tre elementen skiftas ett steg bakåt med hjälp av dess copy-constructor. Tänk på hur mycket extra-arbete detta skulle vara för en vektor med betydligt fler element.

### Åtkomst av element

Du kan komma åt enstaka element via två olika metoder. Låt oss kika på dem både och se hur de skiljer sig åt. Den första är index-operatorn `operator[](IndexType)` och den andra är metoden `at(IndexType)`. Både returnerar referensen till elementet ifråga, så man använder samma metod både för läsning respektive skrivning.

```cpp
struct Dummy {
    inline static int next = 1;
    int value = next++;
    Dummy() { std::println("Dummy({})", value); }
    Dummy(Dummy const&) { std::println("Dummy(Dummy const& {})", value); }
};

int main() {
    auto v = std::vector<Dummy>(3);
    std::println("v[1]: {}", v[1].value);
    v[1].value = 42;
    std::println("v[1]: {}", v[1].value);

    v[6] = Dummy{};
    std::println("v[6]: {}", v[6].value);
    std::println("v.size={}, v.capacity={}", v.size(), v.capacity());

    try { v.at(5) = Dummy{}; }
    catch (std::exception& x) { std::println("ERR: {}", x.what()); }
}
```

```shell
Dummy(1)
Dummy(2)
Dummy(3)
v[1]: 2
v[1]: 42
Dummy(4)
v[6]: 4   // 😮 outside its boundary
v.size=3, v.capacity=3
Dummy(5)
ERR: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/PE1cYTrfW)

Som du ser här ovan, så finns det ingen indexkontroll för `operator[]`, medan metoden `at()` kastar en exception om man försöker gå utanför indexgränsen. Så använd index-operatorn bara när du är helt säker på att du håller dig innanför gränserna, såsom kontroll mot `size()`.

```cpp
for (auto k = 0; k < v.size(); ++k) std::print("{} ", v[k]);
```

### Extraktion av element

Man extraherar element i slutet av en vektor, med hjälp av två metoder. Först `back()`, som returnerar det sista värdet och sen `pop_back()`, som tar bort elementet.

```cpp
struct Dummy {
    inline static int next = 1;
    int value = next++;
    Dummy() { std::println("Dummy({})", value); }
    Dummy(Dummy const& rhs) { 
        std::println("Dummy(Dummy const& {})", rhs.value); 
        value = rhs.value;
    }
};

int main() {
    auto v = std::vector<Dummy>(4);
    auto last = v.back();
    std::println("last: {}, size={}, capacity={}", last.value, v.size(), v.capacity());

    v.pop_back();
    std::println("last: {}, size={}, capacity={}", last.value, v.size(), v.capacity());

    while (not v.empty()) {
        auto last = v.back(); v.pop_back();
        std::println("last: {}, size={}, capacity={}", last.value, v.size(), v.capacity());
    }
}
```

```shell
Dummy(1)
Dummy(2)
Dummy(3)
Dummy(4)
Dummy(Dummy const& 4)
last: 4, size=4, capacity=4     //v.back();
last: 4, size=3, capacity=4     //v.pop_back();
Dummy(Dummy const& 3)
last: 3, size=2, capacity=4
Dummy(Dummy const& 2)
last: 2, size=1, capacity=4
Dummy(Dummy const& 1)
last: 1, size=0, capacity=4
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/jb57W939n)

Om du behöver radera fler element på en gång, och inte "behöver" dessa, är det effektivare att använda metoden `erase()`. Här kommer ett illustrativt exempel.

```cpp
struct Dummy {
    inline static int next = 1;
    int value = next++;
    Dummy() { std::println("{}", to_string("CREATE  "s)); }
    ~Dummy() { std::println("{}", to_string("DISPOSE "s)); }
    //...
    auto to_string(std::string const& op = ""s) const -> std::string {
        if (op.empty())
            return std::format("Dummy({}) @ {}", value, reinterpret_cast<size_t>(this));
        return std::format("{} Dummy({}) @ {}", op, value, reinterpret_cast<size_t>(this));
    }
};

int main() {
    auto v = std::vector<Dummy>(5);
    for (auto&& d : v) std::println("{} ", d.to_string());

    std::println("-- erase --");
    v.erase(v.begin() + 2, v.end());
    std::println("-- done --");
}
```

```shell
CREATE   Dummy(1) @ 168821424
CREATE   Dummy(2) @ 168821428
CREATE   Dummy(3) @ 168821432
CREATE   Dummy(4) @ 168821436
CREATE   Dummy(5) @ 168821440
Dummy(1) @ 168821424 
Dummy(2) @ 168821428 
Dummy(3) @ 168821432 
Dummy(4) @ 168821436 
Dummy(5) @ 168821440 
-- erase --
DISPOSE  Dummy(3) @ 168821432
DISPOSE  Dummy(4) @ 168821436
DISPOSE  Dummy(5) @ 168821440
-- done --
DISPOSE  Dummy(1) @ 168821424
DISPOSE  Dummy(2) @ 168821428
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/z14TnEM5T)

### Användning av `emplace_back()`

Förutom `push_back()` finns det också en snarlik metod som heter `emplace_back()`, vad skiljer dessa åt? Här kommer ett illustrerande exempel.

```cpp
struct Person {
    std::string name{};
    unsigned age{};
    float height{};
    Person() { std::println("Person()"); }
    ~Person() { std::println("~Person({}, {}, {})", name, age, height); }
    Person(std::string n, unsigned a, float h)
        : name{std::move(n)}, age{a}, height{h} 
    {
        std::println("Person({}, {}, {})", name, age, height);
    }
    Person(Person const& rhs) 
        : name{std::move(rhs.name)}, age{rhs.age}, height{rhs.height} 
    {
        std::println("Person const&({}, {}, {})", name, age, height);
    }
    Person(Person&& rhs) noexcept
        : name{std::move(rhs.name)}, age{rhs.age}, height{rhs.height} 
    {
        std::println("Person&&({}, {}, {})", name, age, height);
        rhs.age=0, rhs.height=0;
    }
};

int main() {
    auto v = std::vector<Person>{};
    v.reserve(3);
    {
        std::println("-- copy --");
        auto p = Person{"Anna"s, 42U, 179.5F};
        v.push_back(p);
    }
    {
        std::println("-- move --");
        v.push_back( Person{"Berit"s, 32U, 175.5F} );
    }
    {
        std::println("-- emplace --");
        v.emplace_back( "Carin"s, 52U, 177.5F );
    }
    std::println("-- print --");
    for (auto&& p : v) std::println("Person({}, {}, {})", p.name, p.age, p.height);
    std::println("-- done --");
}
```

```shell
-- copy --
Person(Anna, 42, 179.5)
Person const&(Anna, 42, 179.5)
~Person(Anna, 42, 179.5)
-- move --
Person(Berit, 32, 175.5)
Person&&(Berit, 32, 175.5)
~Person(, 0, 0)
-- emplace --
Person(Carin, 52, 177.5)
-- print --
Person(Anna, 42, 179.5)
Person(Berit, 32, 175.5)
Person(Carin, 52, 177.5)
-- done --
~Person(Anna, 42, 179.5)
~Person(Berit, 32, 175.5)
~Person(Carin, 52, 177.5)
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/4zqWrzW1d)

Som du kan se här ovan, tar `emplace_back()` konstruktor-parametrar för element-typen Person och initierar objektet direkt på plats i den interna array:en hos std::vector objektet. I det två första fallen skapas ett extra objekt på stacken, som sen destrueras. Jämför du dessa två alternativ, ser du att har man ett namngivet objekt så anropas copy-constructor, men skickar man ett anonymt objekt som argument, så anropas move-constructor.

### Vitsen med `resize()`

Jag har tidigare i denna artikel beskrivit metoden `reserve()` och dess betydelse för minnesanvändningen. Men det finns en snarlik metod som heter `resize()`, och hur fungerar den och när är den användbar? Låt oss kika på ett exempel, som använder en STL algoritm-funktion.

```cpp
struct Person {
    std::string name{};
    unsigned age{};
    float height{};
    inline static int next_id = 1;
    Person() { 
        name = "person-"s + std::to_string(next_id++);
        std::println("Person({})", name); 
    }
    //...
};

int main() {
    auto const N = 3;
    {
        std::println("-- reserve --");
        auto v = std::vector<Person>{};
        v.reserve(N);
        std::for_each(v.begin(), v.end(), [](auto&& p){
            std::println("Person({}, {}, {})", p.name, p.age, p.height);
        });
    }
    {
        std::println("-- resize --");
        auto v = std::vector<Person>{};
        v.resize(N);
        std::for_each(v.begin(), v.end(), [](auto&& p){
            std::println("Person({}, {}, {})", p.name, p.age, p.height);
        });
    }
}
```

```shell
-- reserve --
-- resize --
Person(person-1)
Person(person-2)
Person(person-3)
Person(person-1, 0, 0)
Person(person-2, 0, 0)
Person(person-3, 0, 0)
~Person(person-1, 0, 0)
~Person(person-2, 0, 0)
~Person(person-3, 0, 0)
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/M9W3ozTGE)

Reserve allokerar ett internt minnesblock, men det påverkar bara capacity men inte size. Medan resize gör detta plus initierar samtliga element med default constructor. Det är därför vi inte ser några utskrifter i första fallet, eftersom vektorn är tom.

### Varför anropa `shrink_to_fit()`

Säg att du först populerar en vektor med många element och sen tar bort ett flertal av dessa. Då har du ett internt minnesblock i vektorn, med ganska mycket onödigt minne. Det här har betydelse om vektorn sen används mycket och länge i ditt program. I det läget kan det vara en god idé att anropa metoden `shrink_to_fit()`, som trimmar ned det interna minnesblocket. Här kommer ett belysande exempel.

```cpp
int main() {
    auto const N = 1000;
    auto v = std::vector<int>{};
    std::println("[empty]  v.size={:4}, v.capacity={:4}", v.size(), v.capacity());

    v.resize(N);
    std::println("[resize] v.size={:4}, v.capacity={:4}", v.size(), v.capacity());

    v.erase(v.begin() + N/2, v.end());
    std::println("[erase]  v.size={:4}, v.capacity={:4}", v.size(), v.capacity());

    v.shrink_to_fit();
    std::println("[shrink] v.size={:4}, v.capacity={:4}", v.size(), v.capacity());
}
```

```shell
[empty]  v.size=   0, v.capacity=   0
[resize] v.size=1000, v.capacity=1000
[erase]  v.size= 500, v.capacity=1000
[shrink] v.size= 500, v.capacity= 500
```

 [![Compiler Explorer](/images/compiler-explorer-logo.svg "Compiler Explorer - Try it yourself") Try it yourself on Compiler Explorer](https://godbolt.org/z/jeP3faMs8)
