
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 sammanfogningappend()
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 typerT
, medanbasic_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 ochchar_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
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.