Sökresultat för

Visste du att string kan användas som en vector

12 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
Visste du att string kan användas 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>.

#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();
}
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
#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);
}
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):

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:

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

#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:

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.

#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

----------------------------------------------------------------------------------
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 Try it yourself on Compiler Explorer


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.