Hello everyone, I am looking for feedback and best practices (still learning Rust).
In my use case I have data with structure known on compile time, it's basically table with rows but each row length is different.
In most cases rows are short, but rarely they are pretty long.
So using max rows length for all the rows in 2d array is not efficient, there will be a lot of unused allocated memory.
Still, I don't want to use Vecs because I already know the data on compile time and want to go as static as possible.
So here is my first iteration of VariableRowArray collection.
pub struct VariableRowArray<T, const SIZE: usize, const ROWS: usize> {
data: [T; SIZE], // The main data array where rows are stored.
index: [usize; ROWS], // An index array to track the start of each row in the data array.
num_rows: usize, // The current number of rows in the array.
}
impl<T: Default + std::marker::Copy, const SIZE: usize, const ROWS: usize>
VariableRowArray<T, SIZE, ROWS>
{
// Create a new VariableRowArray with default values.
pub fn new() -> Self {
VariableRowArray {
data: [T::default(); SIZE],
index: [0; ROWS],
num_rows: 0,
}
}
pub fn add_row(&mut self, row: &[T]) {
// Calculate the start index for the new row.
let start = if self.num_rows > 0 {
self.index[self.num_rows - 1]
} else {
0 // first row always has 0 start index so we don't track it
};
// Copy each element from the input row to the data array.
for (i, &item) in row.iter().enumerate() {
self.data[start + i] = item;
}
// Update the index array beforehand to mark the start of the row which will be added next time.
// for last possible row will be used in get_row method only as end of slice.
self.index[self.num_rows] = start + row.len();
self.num_rows += 1; // Increment the number of rows.
}
pub fn get_row(&self, row_index: usize) -> Option<&[T]> {
if row_index < self.num_rows {
// Calculate the start and end indices of the requested row.
let start = if row_index > 0 {
self.index[row_index - 1]
} else {
0 // first row always has 0 start index so we don't track it
};
let end = self.index[row_index];
// Return a reference to the slice containing the row's data.
Some(&self.data[start..end])
} else {
None // Row index is out of bounds.
}
}
// Get the current number of rows in the array.
pub fn num_rows(&self) -> usize {
self.num_rows
}
}
usage:
fn main() {
// Create a new VariableRowArray with a maximum data size of 9 elements
// and support for up to 3 rows.
let mut array: VariableRowArray<i32, 9, 3> = VariableRowArray::new();
// Add rows to the array
let row1 = [1, 2, 3];
let row2 = [4, 5];
let row3 = [6, 7, 8, 9];
array.add_row(&row1);
array.add_row(&row2);
array.add_row(&row3);
// Retrieve and print rows
for i in 0..array.num_rows() {
if let Some(row) = array.get_row(i) {
println!("Row {}: {:?}", i, row);
} else {
println!("Row {} not found.", i);
}
}
}
It uses two arrays:
- data - all the rows glued together without gaps so memory is allocated efficiently
- index - to track each row position in the data array in order to access it quickly.
I use const generic prams to enforce user code define all the size on compile time (because it's my goal)
As I see I didn't introduce any more complexity compared to vanilla array, should work with very little overhead and compiler should be happy to optimize everything very well.
Please rate&suggest on above implementation. It feels for me like reinventing the wheel, maybe there is a crate for this?
Is it fine to return option from get_row or better to panic on out of range index?