Föregående artikel handlade om buggar Rust-kompilatorn stoppar. Den här handlar om något mer grundläggande: hur koden ser ut innan man ens hunnit skriva en bugg. Två språk som båda kompilerar till maskinkod och båda kallar sig systemspråk kan se förvånansvärt olika ut rad för rad — och skillnaderna är sällan godtyckliga. De flesta går att spåra till ett designbeslut om vad som ska vara standard och vad som ska krävas explicit.
Jag går igenom åtta sådana skillnader, grupperade i tre delar:
Defaults som skiljer sig
- Immutable vs. mutable
- Move-semantics vs. copy-semantics
Blockens och satsernas form
- Parenteser som försvinner, klamrar som blir obligatoriska
- Ingen C-loop kvar
- Blocket som är ett uttryck
- Pilen är obligatorisk, inte kosmetisk
Deklarationer och typer
- Bitbredden står i typnamnet
- Ingen overloading, inga default-argument
1. Muterbar som standard — eller inte
Börjar vi med den mest grundläggande skillnaden av alla, för den sätter tonen för
resten av artikeln. I C++ är en variabel muterbar (mutable) om du inte säger något annat. I Rust
är den oföränderlig (immutable) om du inte säger något annat. Samma rad kod, total = 1200, betyder
alltså två olika löften beroende på språk.
int total = 1200; // the list price, or so the name suggests
total = apply_discount(total, 20); // "just applying the campaign discount"
total = apply_discount(total, 10); // ...six months later, loyalty stacks on top
$ g++ -std=c++23 -Wall -Wextra mutability.cxx -o mutability-c++ && ./mutability-c++
list price: 1200
after campaign: 960
after loyalty: 864
Inget av detta är fel — total ska uppenbarligen räknas om. Men språket gör ingen
skillnad mellan en variabel du avsett ska ändras och en du råkat skriva till en gång
för mycket. Verktyget för att uttrycka avsikten finns, const, men det är opt-in, en
extra sak att komma ihåg per variabel: (const int total = 1200;)
$ g++ -std=c++23 -Wall -Wextra mutability-const.cxx -o mutability-const-c++
mutability-const.cxx:12:11: error: assignment of read-only variable ‘total’
Samma program i Rust, fast utan mut:
let total = 1200;
total = apply_discount(total, 20);
total = apply_discount(total, 10);
$ rustc --edition 2024 mutability.rs -o mutability-rust
error[E0384]: cannot assign twice to immutable variable `total`
--> mutability.rs:10:5
|
6 | let total = 1200;
| ----- first assignment to `total`
...
10 | total = apply_discount(total, 20);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
6 | let mut total = 1200;
| +++
Ingen const att komma ihåg — let är const tills du säger något annat. Vill du
faktiskt att total ska räknas om, vilket är fallet här, skriver du let mut total och
menar det: (let mut total = 1200;)
$ rustc --edition 2024 mutability-fixed.rs -o mutability-fixed-rust && ./mutability-fixed-rust
list price: 1200
after campaign: 960
after loyalty: 864
Samma utskrift som C++-versionen. Skillnaden är inte vad programmet gör, utan att en läsare som bara ser deklarationsraden redan vet om variabeln kommer att ändras längre ner — utan att behöva läsa resten av funktionen för att kolla.
2. Kopiera som standard — eller flytta
Samma rad, auto b = a; respektive let b = a;, betyder olika saker i de två språken.
Föregående artikels första avsnitt visade vad som händer när den skillnaden biter en
i efterhand; här handlar det bara om själva defaulten.
std::vector<int> a = {1, 2, 3, 4, 5};
auto b = a; // copies the whole vector
b.push_back(6);
$ g++ -std=c++23 -Wall -Wextra assignment.cxx -o assignment-c++ && ./assignment-c++
a has 5 elements
b has 6 elements
b blir en egen kopia. Att lägga till ett element i b påverkar inte a, vilket är
precis vad man väntar sig — men det innebär också att hela vektorn kopierades i tysthet,
oavsett om avsikten var det eller inte.
let a = vec![1, 2, 3, 4, 5];
let mut b = a; // moves ownership of the vector, does not copy it
b.push(6);
$ rustc --edition 2024 assignment.rs -o assignment-rust
error[E0382]: borrow of moved value: `a`
--> assignment.rs:7:35
|
2 | let a = vec![1, 2, 3, 4, 5];
| - move occurs because `a` has type `Vec`, which does not implement the `Copy` trait
3 | let mut b = a; // moves ownership of the vector, does not copy it
| - value moved here
...
7 | println!("a has {} elements", a.len());
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let mut b = a.clone(); // moves ownership of the vector, does not copy it
| ++++++++
let b = a; flyttar äganderätten, den kopierar inte. Efter den raden finns det ingen
a kvar att läsa, och kompilatorn säger det innan något ens körts.
Kompilatorns eget förslag pekar rakt på lösningen — .clone():
$ rustc --edition 2024 assignment-clone-fixed.rs -o assignment-fixed-rust && ./assignment-fixed-rust
a has 5 elements
b has 6 elements
Samma utskrift som C++, och nu ser man den omvända bilden av avsnitt 1. I C++ ber du om
att slippa en kopiering, med std::move. I Rust ber du om att få en, med
.clone(). Vilket som är standard är den egentliga skillnaden — inte att någotdera
alternativet skulle saknas.
std::vector<int> a = {1, 2, 3, 4, 5};
auto b = std::move(a); // moves the content of the vector
b.push_back(6);
3. Parenteser som försvinner, klamrar som blir obligatoriska
C++ kräver parenteser runt villkoret i if/while men gör klamrarna runt kroppen
valfria — en enda sats behöver inga. Rust gör precis tvärtom: inga parenteser, men
klamrarna är obligatoriska. Det här avsnittet handlar om vad den bytesaffären köper dig.
void log_access(bool is_admin) {
if (is_admin)
std::cout << "admin check passed\n";
std::cout << "access granted\n"; // indentation lies: this always runs
}
$ g++ -std=c++23 -Wall -Wextra access.cxx -o access-c++ && ./access-c++
access.cxx: In function ‘void log_access(bool)’:
access.cxx:4:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]
4 | if (is_admin)
| ^~
access.cxx:6:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
6 | std::cout << "access granted\n"; // indentation lies: this always runs
| ^~~
-- as admin --
admin check passed
access granted
-- as guest --
access granted
access granted skrivs ut även för gästen — raden låg aldrig inne i if:et, trots
indenteringen. Till g++:s heder: -Wmisleading-indentation (del av -Wall sedan GCC 6)
fångar faktiskt den här. Men det är en varning, inte ett fel, så programmet kompilerar
och kör med buggen kvar ändå.
Det finns ingen C++-formad variant av det här programmet att ens försöka i Rust — att utelämna klamrarna är ett parsningsfel, inte en stilfråga:
fn log_access(is_admin: bool) {
if is_admin
println!("admin check passed");
}
$ rustc --edition 2024 access.rs -o access-rust
error: expected `{`, found `println`
--> access.rs:3:9
|
3 | println!("admin check passed");
| ^^^^^^^ expected `{`
|
note: the `if` expression is missing a block after this condition
--> access.rs:2:8
|
2 | if is_admin
| ^^^^^^^^
help: you might have meant to write this as part of a block
Med klamrarna utskrivna, precis som Rust kräver:
fn log_access(is_admin: bool) {
if is_admin {
println!("admin check passed");
}
println!("access granted");
}
Så går kompileringen genom
$ rustc --edition 2024 access-fixed.rs -o access-fixed-rust && ./access-fixed-rust
-- as admin --
admin check passed
access granted
-- as guest --
access granted
Samma beteende som C++-versionen — att access granted skrivs ut ovillkorligen var
aldrig själva buggen. Men eftersom scopet måste stavas ut i klamrar istället för att
läsas ur indenteringen finns det inget kvar för en -Wmisleading-indentation-liknande
varning att hitta: koden och indenteringen kan inte säga emot varandra.
4. Ingen C-loop kvar
Rust saknar den klassiska tredelade for (init; villkor; steg) helt — det finns bara
for pattern in iterator, while och loop. Så här ser det ut om man av vana sträcker
sig efter C++-formen, och vad den idiomatiska ersättningen blir istället.
int sum = 0;
for (int i = 0; i < 10; i += 2) {
sum += i;
}
$ g++ -std=c++23 -Wall -Wextra stepping.cxx -o stepping-c++ && ./stepping-c++
sum = 20
Samma sak, försökt skrivet i Rust:
let mut sum = 0;
for (let mut i = 0; i < 10; i += 2) {
sum += i;
}
$ rustc --edition 2024 stepping.rs -o stepping-rust
error: expected pattern, found `let`
--> stepping.rs:3:10
|
3 | for (let mut i = 0; i < 10; i += 2) {
| ^^^
|
help: remove the unnecessary `let` keyword
|
3 - for (let mut i = 0; i < 10; i += 2) {
3 + for (mut i = 0; i < 10; i += 2) {
|
error: expected one of `)`, `,`, `@`, `if`, or `|`, found `=`
--> stepping.rs:3:20
|
3 | for (let mut i = 0; i < 10; i += 2) {
| ^ expected one of `)`, `,`, `@`, `if`, or `|`
Parsern försöker läsa for (...) som ett mönster att packa upp, för det är det
enda som får stå mellan for och in. Det finns ingen tredelad header att tolka in
i — den har aldrig funnits i grammatiken.
let mut sum = 0;
for i in (0..10).step_by(2) {
sum += i;
}
$ rustc --edition 2024 stepping-fixed.rs -o stepping-fixed-rust && ./stepping-fixed-rust
sum = 20
(0..10).step_by(2) ersätter init/villkor/steg med ett intervall och en adapter. Ingen
räknare att deklarera för hand, inget off-by-one att göra fel på vid gränsen — och
inget att glömma initiera, vilket för oss tillbaka till avsnitt 1.
5. Blocket som är ett uttryck
I Rust kan if/else (och vilket { }-block som helst) vara ett värde — sista
uttrycket i blocket, utan semikolon på slutet, är det som blocket blir. I C++ är if/else
en sats; den kan inte lämna tillbaka något värde, så man tar antingen till
villkorsoperatorn eller deklarerar en muterbar variabel i förväg och tilldelar den i
varje gren.
auto classify(int celsius) -> std::string {
std::string label; // has to exist before we know the answer
if (celsius < 0) {
label = "freezing";
} else if (celsius < 15) {
label = "cold";
} else if (celsius < 25) {
label = "mild";
} else {
label = "hot";
}
return label;
}
$ g++ -std=c++23 -Wall -Wextra classify.cxx -o classify-c++ && ./classify-c++
-5 C -> freezing
10 C -> cold
20 C -> mild
30 C -> hot
Fyra grenar är en för många för en läsbar kedja av villkorsoperatorer, så label
måste finnas innan någon gren körts, tilldelas i var och en, och returneras efteråt.
Tre separata saker att få rätt — och avsnitt 1 visade redan vart "finns innan vi vet
svaret" kan leda.
let label = if celsius < 0 {
"freezing"; // stray semicolon: this arm now yields ()
} else if celsius < 15 {
"cold"
} else if celsius < 25 {
"mild"
} else {
"hot"
};
$ rustc --edition 2024 classify.rs -o classify-rust
error[E0308]: `if` and `else` have incompatible types
--> classify.rs:4:12
|
2 | let label = if celsius < 0 {
| -------------- `if` and `else` have incompatible types
3 | "freezing"; // stray semicolon: this arm now yields ()
| -----------
| | |
| | help: consider removing this semicolon
| expected because of this
4 | } else if celsius < 15 {
| ____________^
5 | | "cold"
6 | | } else if celsius < 25 {
7 | | "mild"
8 | | } else {
9 | | "hot"
10 | | };
| |_____^ expected `()`, found `&str`
Ett enda extra ; efter "freezing" gör att den grenens värde blir () — enhetstypen,
Rusts "ingenting", inte C++:s void, och den deltar i typkontrollen precis som vilken
annan typ som helst — istället för &str. De andra tre grenarna ger fortfarande &str,
så grenarna är oense och hela if-uttrycket faller på typkontrollen. Semikolonet är
alltså inte kosmetiskt här; det är skillnaden mellan ett uttryck och en sats.
Så här ser det fungerande programmet ut i Rust:
fn classify(celsius: i32) -> &'static str {
if celsius < 0 { "freezing" }
else if celsius < 15 { "cold" }
else if celsius < 25 { "mild" }
else { "hot" }
}
fn main() {
for t in [-5, 10, 20, 30] {
println!("{t} C -> {}", classify(t));
}
}
$ rustc --edition 2024 classify-fixed.rs -o classify-fixed-rust && ./classify-fixed-rust
-5 C -> freezing
10 C -> cold
20 C -> mild
30 C -> hot
Inga semikolon på grenarnas värden, och ingen label-variabel alls — kedjan av
if/else if/else är sista uttrycket i funktionskroppen, så den är
returvärdet. Inget förhandsdeklarerat, inget omtilldelat, inget att lämna oinitierat
på en gren någon glömmer.
6. Pilen är obligatorisk, inte kosmetisk
C++ har tre likvärdiga sätt att skriva samma funktion: den traditionella
returtypen först, pil-formen -> T (C++11), och — sedan C++14 — ingen utskriven
returtyp alls, härledd ur kroppen. Rust har bara ett sätt: -> T finns och stämmer,
eller så returnerar funktionen () och inget typkontrolleras.
int add_traditional(int a, int b) {
return a + b;
}
auto add_trailing(int a, int b) -> int {
return a + b;
}
auto add_inferred(int a, int b) { // C++14: return type deduced, no arrow needed
return a + b;
}
$ g++ -std=c++23 -Wall -Wextra add.cxx -o add-c++ && ./add-c++
5
5
5
Alla tre gör samma sak och kompilerar likadant. Pilen i auto add_trailing(int a, int b) -> int är ett stilval — add_inferred bevisar att man inte ens behöver
skriva ut någon returtyp, C++ räknar ut den från return a + b;.
fn add(a: i32, b: i32) { // no "-> i32": does this get inferred, like C++'s auto?
a + b
}
$ rustc --edition 2024 add.rs -o add-rust
error[E0308]: mismatched types
--> add.rs:2:5
|
1 | fn add(a: i32, b: i32) { // no "-> i32": does this get inferred, like C++'s auto?
| - help: try adding a return type: `-> i32`
2 | a + b
| ^^^^^ expected `()`, found `i32`
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> add.rs:6:20
|
6 | println!("{}", add(2, 3));
| -- ^^^^^^^^^ `()` cannot be formatted with the default formatter
Att utelämna -> i32 utlöser inte samma sorts inferens som C++14:s auto. En fn
utan pil betyder att den returnerar (), punkt slut — kroppens sista uttryck
a + b blir då ett typfel, eftersom det är en i32 som hamnar där () förväntades.
$ rustc --edition 2024 add-fixed.rs -o add-fixed-rust && ./add-fixed-rust
5
Det finns inga andra sätt att skriva den här funktionen på. Varje fn som
returnerar ett värde stavar ut -> T, så signaturen ensam berättar alltid svaret —
man behöver aldrig läsa in i kroppen (eller lita på att en kommentar stämmer) för att
ta reda på vad en funktion returnerar.
7. Bitbredden står i typnamnet
C++:s heltalsnamn (int, char, size_t) säger inget om maskinstorleken (bytes och-eller bits)
för respektive type — det överlåts åt plattformen, och språket konverterar mellan dem utan att bli
tillfrågat. Rusts typnamn (i32, u8, usize) anger maskinstorleken utan krusiduller, och det
finns ingen implicit konvertering mellan två heltalstyper, någonsin.
int big = 1000;
unsigned char narrowed = big; // implicit narrowing, no cast asked for
$ g++ -std=c++23 -Wall -Wextra narrow.cxx -o narrow-c++ && ./narrow-c++
big = 1000
narrowed = 232
unsigned char narrowed = big; kompilerar helt utan diagnostik under
-Wall -Wextra — det krävs -Wconversion specifikt för att bli varnad. 1000 ryms
inte i 8 bitar, så det wrappar (modulo-aritmetik): 1000 % 256 = 232.
let big: i32 = 1000;
let narrowed: u8 = big; // no implicit conversion between integer types, ever
$ rustc --edition 2024 narrow.rs -o narrow-rust
error[E0308]: mismatched types
--> narrow.rs:3:24
|
3 | let narrowed: u8 = big; // no implicit conversion between integer types, ever
| -- ^^^ expected `u8`, found `i32`
| |
| expected due to this
|
help: you can convert an `i32` to a `u8` and panic if the converted value doesn't fit
|
3 | let narrowed: u8 = big.try_into().unwrap(); // ...
i32 och u8 är olika typer, punkt slut — det spelar ingen roll att just den här
konverteringen råkar vara krympande; Rust avvisar all implicit heltalskonvertering,
även breddande sådan. Lägg märke till vad rustc föreslår: try_into().unwrap(), som
panikar istället för att tyst wrappa om värdet inte ryms. Det är det "säkra"
alternativet — och medvetet inte det den fixade versionen nedan använder.
$ rustc --edition 2024 narrow-fixed.rs -o narrow-fixed-rust && ./narrow-fixed-rust
big = 1000
narrowed = 232
as u8 är den explicita, krympande casten — samma wraparound-aritmetik som
C++-versionen, samma 232. Utskriften är identisk med C++-programmet; skillnaden är
att as fick skrivas ut för hand, exakt på det stället där krympningen sker, istället
för att ske implicit varhelst en bredare typ möter en smalare.
Och för att visa att "säkert" ska tas bokstavligt: samma big, samma try_into()
som rustc föreslog ovan — men den panikar, den wrappar inte:
let big: i32 = 1000;
let narrowed: u8 = big.try_into().unwrap();
$ rustc --edition 2024 narrow-panic.rs -o narrow-panic-rust && ./narrow-panic-rust
thread 'main' (353814) panicked at narrow-panic.rs:3:39:
called `Result::unwrap()` on an `Err` value: TryFromIntError(PosOverflow)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Samma indata som fick as u8 att tyst bli 232 får try_into().unwrap() att krascha
programmet. Ingen av dem är "rätt" i något absolut mening — as är rätt när wrappningen
är avsedd (kryptografi, hashning, bitmanipulation), try_into() är rätt när ett värde
utanför intervallet är ett programmeringsfel som ska stoppa exekveringen direkt istället
för att tyst ge fel svar.
8. Ingen overloading, inga default-argument
C++ löser greet(name) och greet(name, greeting) som två olika funktioner beroende
på anropsstället. Rust har ingen overloading och inga default-värden på parametrar —
ett namn betyder en signatur.
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!\n";
}
void greet(const std::string& name, const std::string& greeting) {
std::cout << greeting << ", " << name << "!\n";
}
$ g++ -std=c++23 -Wall -Wextra greet.cxx -o greet-c++ && ./greet-c++
Hello, Ada!
Hej, Ada!
Två funktioner som heter greet, upplösta efter antal argument vid varje
anropsställe. Inget ovanligt med det i C++ — rutin.
fn greet(name: &str) {
println!("Hello, {name}!");
}
fn greet(name: &str, greeting: &str) {
println!("{greeting}, {name}!");
}
$ rustc --edition 2024 greet.rs -o greet-rust
error[E0428]: the name `greet` is defined multiple times
--> greet.rs:5:1
|
1 | fn greet(name: &str) {
| -------------------- previous definition of the value `greet` here
...
5 | fn greet(name: &str, greeting: &str) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `greet` redefined here
|
= note: `greet` must be defined only once in the value namespace of this module
error[E0061]: this function takes 1 argument but 2 arguments were supplied
--> greet.rs:11:5
|
11 | greet("Ada", "Hej");
| ^^^^^ ----- unexpected argument #2 of type `&'static str`
Två fn med samma namn är ett hårt fel, inte overload-upplösning — greet upptar
en enda plats i modulens namnrymd oavsett parameterlista. Det andra felet är en
följdeffekt: med bara enparametersvarianten kvar matchar inte längre anropet med två
argument något alls.
$ rustc --edition 2024 greet-fixed.rs -o greet-fixed-rust && ./greet-fixed-rust
Hello, Ada!
Hej, Ada!
Två separata namn, greet och greet_with, där greet implementeras i termer av
greet_with. Samma utskrift som C++-versionen. Anropsstället måste nu säga vilken av
dem det menar — det finns ingen aritetsbaserad dispatch att luta sig mot, så de två
beteendena får två namn istället för att dela på ett.
fn greet(name: &str) {
greet_with("Hello", name);
}
fn greet_with(greeting: &str, name: &str) {
println!("{greeting}, {name}!");
}
fn main() {
greet("Ada");
greet_with("Hej", "Ada");
}
Och vad blir kvar av det?
Åtta punkter, men egentligen bara en handfull grundfrågor som upprepas i olika skepnad: vad är standard och vad måste man be om, och vad räknas som ett värde och vad räknas bara som en instruktion. Muterbarhet (1), kopiering (2) och heltalskonvertering (7) är samma fråga — "vad händer om jag inte säger något" — ställd om variabler, äganderätt respektive typer. Parenteser (3), C-loopen (4) och uttrycksblock (5) är en annan variant av samma sak: hur mycket av satsens form är förhandlingsbar och hur mycket är fast. Pilen (6) och overloading (8) handlar båda om att en signatur ska gå att lita på utan att läsa kroppen.
Inget av det här är buggar i den mening föregående artikel handlade om — all exempelkoden i C++-spalten är fullt korrekt, avsiktligt skriven kod. Skillnaden är att C++ oftast ger dig ett val och en rimlig default, medan Rust oftare bara ger dig ett sätt. Det är bekvämt fram tills dagen du vill ha det andra sättet, och det är precis den avvägningen språkdesigners gör hela tiden — bara åt olika håll.
Om något av det här kändes som en detalj: det är poängen. Ingen enskild punkt är avgörande på egen hand. Tillsammans är de anledningen till att portering av kod, eller att bara växla mellan språken samma dag, ger den där låga bakgrundsfriktionen — fingrarna vet ett mönster, kompilatorn vill ha ett annat.
Saker jag hoppat över i denna artikel
Du har säkert noterat att det finns en del överhoppade begrepp i texten, såsom panic,
.unwrap(), &str med flera. Under hösten kommer jag att beskriva dessa och flera andra
i kommande artiklar.
Hur pass intresserad av Rust är du? Svara på en kort enkät!
Länkar
Senaste Artiklarna
-
Rust syntax jämfört med C++
14 augusti 2026 -
Fem skäl att välja Rust över C++
10 augusti 2026 -
Apollo 11 och kvinnan bakom koden
12 april 2026 -
Hur gör man en Ralph Loop?
6 april 2026 -
Kan du alla nya AI termer?
22 mars 2026 -
OpenClaw - Det ska vara en hummer i år
15 mars 2026 -
Så här kopplar du SMHI till Claude AI
16 februari 2026 -
Asynkrona operationer med coroutines
21 januari 2026