I have been a C/C++ developer for long time. I have recently worked in Rust for a client. And I found some of the aspects of Rust worth exploring with comparison to C++. In this post, I will explore how and where Rust does things differently than C++. Where are the possible limitations in C++? I will try to address some of the questions that come to a programmer’s mind when they hear that “Rust provides memory safety.” In this post, I will focus on one aspect that has caused a lot of grief in the industry: out-of-bounds array access and the problems associated with it.
Out of Bound Array Access in C++
Let us take a look at the following example code written in C++.
#include <cstddef>
#include <iostream>
int main()
{
int values[]{10, 20, 30};
std::size_t index{};
std::cout << "Enter an index (0-2): ";
if (!(std::cin >> index))
{
std::cerr << "Invalid input\n";
return 1;
}
std::cout << "Value: " << values[index] << '\n';
return 0;
}Compile and Run
$ g++ -std=c++20 -Wall -Wextra -Wpedantic -O0 \
cpp_bounds.cpp -o cpp_bounds
$ g++ -std=c++20 -Wall -Wextra -Wpedantic -O0 \
cpp_bounds.cpp -o cpp_bounds
$ ./cpp_bounds
Enter an index (0-2): 3
Value: -1242246144As you can see in this example code we have asked the user to enter an index to access in the array values which has a size 3, and when the user entered 3 (which is out of the bound of the array values), the code prints some garbage values. The reason is the array index 3 doesn’t belong to the array values and hence this is an invalid (out of bound array) access and C++ is happily letting this go unchecked. This code was run on an Ubuntu machine. There is no guarantee what would be the outcome of this access, the behaviour is undefined. The code may crash and the language doesn’t dictate the behaviour in this sitaution.
Address Sanitiser
C++ provides something called AddressSanitizer to flag this issue though and it can be used as below:
$ g++ -std=c++20 -Wall -Wextra -Wpedantic -O0 -g \
-fsanitize=address,undefined \
-fno-omit-frame-pointer \
cpp_bounds.cpp -o cpp_bounds_asan
$ ./cpp_bounds_asan
Enter an index (0-2): 3
cpp_bounds.cpp:17:43: runtime error: index 3 out of bounds for type
'int [3]'So with the use of -fsanitize=address,undefined compilation flag we can highlight this issue using a C++ compiler. But the point is C++ doesn’t have any mechanism to detect the out of bound access of array element and hence the code may crash when situation like this arises.
Out-of-bounds array access in Rust
Now lets take a look at the following Rust code doing very similar thing as the CPP code earlier:
use std::io;
fn main() {
let values = [10, 20, 30];
let mut input = String::new();
println!("Enter an index (0-2):");
io::stdin()
.read_line(&mut input)
.expect("Failed to read input");
let index: usize = input.trim().parse().expect("Please enter a valid number");
println!("Value: {}", values[index]);
}Compile and Run
$ rustc --edition=2021 rust_bounds.rs -o rust_bounds
$ ./rust_bounds
Enter an index (0-2):
3
thread 'main' (172005) panicked at rust_bounds.rs:15:27:
index out of bounds: the len is 3 but the index is 3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceSo as you can see the Rust code has panicked and saying “index out of bound” which C++ code didn’t. To verify this let’s look at the back trace.
Show the Rust Backtrace
To see the Rust panic backtrace, set the RUST_BACKTRACE environment variable to 1 and run the program again:
$ export RUST_BACKTRACE=1
$ ./rust_bounds
Enter an index (0-2):
3
thread 'main' (168177) panicked at rust_bounds.rs:18:27:
index out of bounds: the len is 3 but the index is 3
stack backtrace:
0: __rustc::rust_begin_unwind
at /rustc/library/std/src/panicking.rs:689:5
1: core::panicking::panic_fmt
at /rustc/library/core/src/panicking.rs:80:14
2: core::panicking::panic_bounds_check
at /rustc/library/core/src/panicking.rs:271:5
3: rust_bounds::main
4: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full`
for a verbose backtrace.
Note in the above backtrace just after the main(), Rust is doing an explicit bound check operation in the function core::panicking::panic_bounds_check. But the aplication is still panicking. So how does that help us?
Rust Code Still Panics
Yes Rust code still panics when it finds the array index is out of bound. So what is the benefit then? The point is Rust explicitly checks the bound and deterministically kills the program which is a defined behaviour unlike C++ which categorises it as undefined behaviour.
Handling an out-of-bounds index in Rust
To handle an out-of-bounds index without panicking, you can rewrite the Rust code as follows:
use std::io;
fn main() {
let values = [10, 20, 30];
let mut input = String::new();
println!("Enter an index (0-2):");
io::stdin()
.read_line(&mut input)
.expect("Failed to read input");
let index: usize = input.trim().parse().expect("Please enter a valid number");
match values.get(index) {
Some(value) => println!("Value: {value}"),
None => println!("Index {index} is out of bounds"),
}
}Notice the following piece of code that has been added to the previous version:
match values.get(index) {
Some(value) => println!("Value: {value}"),
None => println!("Index {index} is out of bounds"),
}values.get(index)
The get() method attempts to retrieve the element at index. Instead of panicking when the index is invalid, it returns an Option<&i32>:
Some(&value)if the element exists.Noneif the index is out of bounds.
For example:
values.get(1) // Some(&20)
values.get(5) // Nonematch
match examines which Option variant was returned:
match values.get(index)Valid index
For a valid index, values.get(index) returns Some(value). The match expression then selects the Some arm and prints the value stored at that index, as shown below:
Some(value) => println!("Value: {value}")If the index is invalid, values.get(index) returns None. The match expression then selects the None arm and prints an error message, as shown below:
None => println!("Index {index} is out of bounds")Manual bounds checking in C++
Could we have opted for some level of similar check in C++ ocde, yes of course we could somewhat in the following way:
if (index < std::size(values))
{
std::cout << "Value: " << values[index] << '\n';
}
else
{
std::cerr << "Invalid index: " << index << '\n';
// Perform the specified safe recovery action.
}But this is manual and to check every array access in this fashion could be strenuous and error prone. C++ wasn’t designed to worry about unbounded array access, it leaves it to the programmer to take care of that. Rust realised this is an area where lot of the pain comes in the development and factored this in. So for Rust it is by default whereas for C++ it has to be eforced manually where possible.
Alternative options in C++
Standard Template Library or STL in C++ privides std::array and std::vector as an alternative option instead of using ray C-style array. If you are using std::array or std::vector C++ offers std::array::at() or std::vector::at() to check the index before using. If the index is out of bound it will throw an exception which needs to be handled. Exception handling has some overhead which you need to keep in mind. Also, in some cases in a resource contrained Embedded System you may not have the luxary of exceptions as in those cases you may have exception disabled. In safety critical software like in defense usage of exceptio is also discouraged.
So on this aspect C++ still has some measures that can be used to avoid out-of-bound situations. Yes like manual index checking could be more manual than Rust and as mentioned earlier could be strenuous too. Or options like std::array::at() or std::vector::at() type methods that may involve exception handling. I cannot say Rust wins hands down in this but definitely has some edge over C++. This was my first comparative exploration between the two and I will keep more like this in coming days.
The source code can be downloaded from my Github repository here.
Discover more from Tech For Talk
Subscribe to get the latest posts sent to your email.
Leave a Reply