# Visste du att string kan användas som en vector

19 augusti 2025 | https://www.ribomation.se/blog/2025/visste-du-att-string-kan-anvandas-som-en-vector/

När man behöver en sekvens-container i C++ så är det givna valet `std::vector`. Jag tänkte emellertid i denna artikel gå utanför de gängse ramarna och visa på ett oväntat alternativ, nämligen `std::string`, eller för att vara mer precis `std::basic_string<T>` eftersom `std::string` är ett alias för `std::basic_string<char>`. Det här verkar ju tokigt inledningsvis, men dessa två containrar har mer gemensamt än vad du tror. Häng med och läs en kul och annorlunda artikel om C++.

Lika som bär?
-------------

Både `std::vector<T>` och `std::basic_string<T>` är:

*   **sekvenser av element** lagrade i ett sammanhängande minnesblock
*   **ägande** containrar (de allokerar och frisläpper själva sitt minne)
*   **random-access**: `operator[]`, `front()`, `back()` fungerar
*   kompatibla med **range-for** och STL-algoritmer

Med andra ord kan man skriva nästan samma kod, oavsett om man använder `vector<int>` eller `basic_string<int>`.

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

int main() {
    auto v = std::vector<int>{1, 2, 3};
    for (auto x : v) std::print("{} ", x);
    std::println();

    auto s = std::basic_string<int>{4, 5, 6};
    for (auto x : s) std::print("{} ", x);
    std::println();
}
```

```text
1 2 3
4 5 6
```

* * *

Men skillnaderna då?
--------------------

### Sammanfogning

`std::basic_string<T>` erbjuder bekväma operatorer för att **slå ihop sekvenser**:

*   `+` och `+=` för sammanfogning
*   `append()` sammalunda

```cpp
#include <string>
#include <print>

int main() {
    auto a = std::basic_string<int>{1, 2, 3};
    auto b = std::basic_string<int>{10, 11, 12};
    auto c = a + b;
    c += {20, 21, 22};
    c.append({30, 31, 32});
    for (int x: c) std::print("{} ", x);
}
```

```text
1 2 3 10 11 12 20 21 22 30 31 32
Process finished with exit code 0
```

Med `std::vector` får man istället använda `insert()` eller `std::copy`.

### Optimering för korta sekvenser

De flesta `std::basic_string`\-implementationer använder **SSO** (_Short String Optimization_) där kortare sekvenser lagras direkt i string objektet och inte som en array på heap:en. För GCC är det 15 element plus 1 för "0-byte".

`std::vector<T>` har ingen motsvarande optimering: även små vektorer kräver alltid en heap-allokering när de växer från tomt innehåll.

### Typinstansiering

Definitionen av `std::basic_string` är (från cppreference.com):

```cpp
template<
    typename CharType,
    typename Traits    = std::char_traits<CharType>,
    typename Allocator = std::allocator<CharType>
> class basic_string;
```

Inget hindrar att man typ-instansierar den med andra typer än `char`:

```cpp
auto numbers = std::basic_string<int>{1, 2, 3};
auto samples = std::basic_string<unsigned short>{10, 20, 30};
auto reals   = std::basic_string<float>{1.5f, 2.5f};
```

### Krav på element-typen

För `std::basic_string<T>` gäller att elementtypen `T` måste vara **trivialt kopierbar**. Det innebär i praktiken att typen:

*   har standardkonstruktor, kopieringskonstruktor, flyttkonstruktor, tilldelningsoperatorer och destruktor som alla är _triviala_ (dvs ingen pekarhantering)
*   inte innehåller virtuella funktioner eller pekare till medlemsfunktioner
*   inte kräver användardefinierad initialisering.

Detta är en starkare begränsning än för `std::vector<T>`, som kan hantera mer komplexa typer.

### std::char\_traits

En annan viktig komponent är `std::char_traits<T>`, som används för att definiera jämförelser, längdberäkningar och andra operationer på element. Den kan specialiseras för att anpassa hur `basic_string<T>` fungerar. För numeriska typer är det sällan nödvändigt att specialisera den, men för anpassade symboltyper kan det vara relevant.

#### Exempel: icke-trivial element-typ

```cpp
#include <iostream>
#include <string>

struct Account {
    std::string accno{};
    double      balance{};
    Account(std::string a, double b) : accno(std::move(a)), balance(b) {}
};

int main() {
    auto s = std::basic_string<Account>{};  //compilation error
}
```

Denna typ är **inte** _trivialt kopierbar_, då den innehåller en `std::string`. Därmed kan `std::basic_string<Account>` inte användas. Däremot fungerar `std::vector<Account>` utmärkt, eftersom `vector` endast kräver att typen är flyttbar/kopierbar. Så här ser kompileringsfelet ut:

```text
error: static assertion failed: static_assert(is_trivially_copyable_v<_CharT>
note: ‘std::is_trivially_copyable_v’ evaluates to false
```

### Standardgränssnittet

`vector` och `basic_string` skiljer sig i små men viktiga detaljer:

*   `string` garanterar null-terminering (för text)
*   `vector` tillåter **alla typer** `T`, medan `basic_string` i praktiken är optimerad för texttyper

* * *

När kan `basic_string<int>` vara vettigt?
-----------------------------------------

*   När man ofta behöver **slå ihop** sekvenser av tal
*   När sekvenserna är **korta** och man vill dra nytta av SSO
*   Som pedagogiskt experiment – för att se hur långt `basic_string` går att använda utanför text

_Men_: för de flesta användningsfall är `std::vector<int>` tydligare och mer idiomatisk. Att använda `basic_string<int>` kan uppfattas som förvirrande för somliga.

* * *

Prestandajämförelse
-------------------

För att jämföra prestanda mellan `std::basic_string<double>` och `std::vector<double>` kan vi använda [Google Benchmark](https://github.com/google/benchmark).

```cpp
#include <benchmark/benchmark.h>
#include <string>
#include <vector>

template <typename Container>
static void BM_push_back(benchmark::State& state) {
    for (auto _ : state) {
        auto c = Container{};
        for (int i = 0; i < 10'000; ++i) {
            c.push_back( static_cast<double>(i) );
        }
        benchmark::DoNotOptimize(c);
    }
}
 
BENCHMARK_TEMPLATE(BM_push_back, std::vector<double>);
BENCHMARK_TEMPLATE(BM_push_back, std::basic_string<double>);
BENCHMARK_MAIN();
```

#### Förväntade resultat

*   `std::vector<double>` tenderar att vara snabbare, eftersom dess implementation är optimerad för numeriska typer.
*   `std::basic_string<double>` har extra overhead (kontroller och `char_traits`), vilket kan göra den långsammare.

#### Aktuellt resultat

Exekvering i Compiler Explorer

```cpp
----------------------------------------------------------------------------------
Benchmark                                        Time             CPU   Iterations
----------------------------------------------------------------------------------
BM_push_back<std::vector<double>>            73160 ns        72139 ns        11589
BM_push_back<std::basic_string<double>>     160662 ns        92078 ns         7284
```

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

* * *

Sammanfattning
--------------

Egenskap

std::vector

std::basic\_string

Minneslayout

Kontiguöst

Kontiguöst

Typkrav

Kopierbar/flyttbar

Trivialt kopierbar

API för delsekvenser

Nej (algoritmer behövs)

Ja (`substr`, `find`)

Sammanfogningsoperatorer

Nej

Ja (`+`, `append`)

SSO

Nej

Ja (vanligtvis)

Anpassning via traits

Nej

Ja (`char_traits`)

### När ska man använda vad?

*   **Använd `std::vector<T>`** när:
    
    *   `T` är en komplex eller icke-trivial typ.
    *   du behöver maximal prestanda för numeriska beräkningar.
    *   du behöver flexibilitet i datatyper.
*   **Använd `std::basic_string<T>`** när:
    
    *   du arbetar med sekvenser av triviala typer.
    *   du vill använda substring- och sökfunktioner direkt.
    *   du har nytta av SSO för små sekvenser.

Kort sagt: `std::basic_string<T>` kan ses som en specialiserad sekvenscontainer med extra funktionalitet för sökning och hantering av triviala element, medan `std::vector<T>` är den generella lösningen för alla typer.
