Strings: C++ and Rust
std::string and String are both owned byte buffers with a length and a capacity, and both are usually UTF-8 in practice. The difference appears at the index: C++ lets a program write one character at a position, and Rust refuses, because a byte position is not a character position in UTF-8.
The two programs append, search and take a prefix. Then the page shows the index assignment that compiles in C++ and stops rustc with an error that points at the two safe alternatives.
The task in C++
The string is built from a literal, appended to with +=, changed at index zero, measured with size, searched with find and cut with substr. Every operation is on bytes and every index is a byte index, which is consistent and fast.
The output shows the capitalised text, its size, the position of the second word and the prefix. The index assignment is safe here because the first byte is a complete ASCII character.
#include <iostream>
#include <string>
int main()
{
std::string text = "hello";
text += " world";
text[0] = 'H';
std::cout << text << '\n';
std::cout << "size " << text.size() << '\n';
std::cout << "found at " << text.find("world") << '\n';
std::cout << text.substr(0, 5) << '\n';
}
g++ -std=c++23 -Wall -Wextra strings.cpp -o strings
./strings
Hello world
size 11
found at 6
Hello
Nothing warns about the index assignment, and nothing would warn if the string began with a multi-byte character. The language trusts the programmer to know where the character boundaries are.
The same task in Rust
The Rust version appends with push_str, measures with len, searches with find and takes a prefix with a range. The search returns an Option, which prints as Some(6) in the debug form, and the prefix is a slice rather than a new string.
The capitalisation is gone from the Rust program on purpose: there is no index assignment to show, and the mistake section explains what replaces it.
fn main() {
let mut text = String::from("hello");
text.push_str(" world");
println!("{text}");
println!("size {}", text.len());
println!("found at {:?}", text.find("world"));
println!("{}", &text[..5]);
}
rustc --edition 2024 strings.rs
./strings
hello world
size 11
found at Some(6)
hello
The two outputs agree apart from the search result form and the capital letter. The prefix is a &str that borrows the original, so it costs nothing, which is the subject of the next page in this set.
What differs and why
Who owns the value: both strings own a heap buffer, and both move when assigned without a clone. The difference is that Rust knows the buffer is UTF-8 and C++ does not, so the Rust methods that take an index either check a boundary or are named as byte operations.
What the compiler rejects: C++ accepts s[0] = 'H' for any position, including one in the middle of a multi-byte character. Rust rejects the same line with E0277, because String has no index that yields a mutable character.
What happens at run time: in C++ the write can break the UTF-8 invariant of the string, and every later reader sees invalid text. In Rust the alternatives, replace_range and a rebuild from bytes, make the invariant explicit.
The habit that breaks
The habit is s[0] = 'H', or reading s[0] and expecting a character. The Rust error is long because it lists the index implementations, but the first line is the answer: the type cannot be indexed by an integer.
fn main() {
let mut text = String::from("hello");
text[0] = 'H';
println!("{text}");
}
rustc --edition 2024 index.rs
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> index.rs:3:10
|
3 | text[0] = 'H';
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
help: `usize` implements trait `SliceIndex<T>`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/core/src/slice/index.rs:214:0
|
= note: `SliceIndex<[T]>`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/core/src/bstr/traits.rs:197:0
|
= note: `SliceIndex<ByteStr>`
= note: required for `String` to implement `Index<{integer}>`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
The C++ lesson for this ground is std::string, and the Rust lesson is String: build, grow and search. The next page in this set is String views and slices.
The C++ program was built with g++ 15.2.0 and the Rust program with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev