I was refactoring some code to move command-line parsing logic from /main.rs into /lib.rs and I ran into a compilation error that has a very solution that demonstrates some of the concepts and issues with ownership and borrowing in rust and how easy it can be to work around some of them, though not always. It shows that with rust, we always need to think about memory allocation, ownership, lifetime, and scope, where we must maintain ownership in scope to keep data valid.
Here was my first attempt at a struct with a method that parses the command line to create an instance of itself.
pub struct WinkConfig { cmd_name: String, verbose: bool, dry_run: bool, command_code: String, export: bool, pretty_print: bool, all_args: Vec<String>, cmd_args: Vec<String>, help_msg: String } impl WinkConfig { pub fn get_from_cmd_line_args() -> WinkConfig { let args: Vec<String> = std::env::args().collect(); let mut dry_run: bool = false; // -d command line option let mut verbose: bool = false; // -v command line option let mut export: bool = false; // -e command line option let mut pretty_print: bool = false; // -p command line option let mut first_arg_index = 1; // number of processed command line arguments (first is command name) let mut help_msg = String::new(); for arg in args.iter().skip(first_arg_index) { if arg == "help" { help_msg = format!("Help requested by {0}", arg); break; } let prefix: char = arg.to_lowercase().chars().next().unwrap(); // if the argument is not help and does not start with a slash or a dash, then it should be a command code if prefix != '/' && prefix != '-' { break; } for char in arg.chars() { match char { '/' | '-' => continue, 'v' => verbose = true, 'd' => dry_run = true, 'p' => pretty_print = true, 'e' => export = true, 'h' | '?' => { help_msg = format!("Help requested by {0}", arg); break; }, _ => { help_msg = format!("Unrecognized command line option: {0}", arg); break; } } } first_arg_index + first_arg_index + 1; } let mut command_code = String::new(); if first_arg_index < args.len() { command_code = args[first_arg_index].to_owned(); first_arg_index = first_arg_index + 1; } WinkConfig { cmd_name: args[0].to_owned(), verbose: verbose, dry_run: dry_run, command_code: command_code, export: export, pretty_print: pretty_print, all_args: args, cmd_args: (&args[first_arg_index..]).to_vec(), help_msg: help_msg, } } }
This may not be the rustiest code, but it should function. The last block might look unfamiliar to developers coming from languages such as C#. Instead of returning an expression with a return statement and a semicolon, rust prefers raw expressions without semicolons to end functions.
The compilation error is in this last block that creates the WinkConfig to return. Can you see the defect? Trust me, I would not have noticed or even partially understood this a few days ago.
error[E0382]: borrow of moved value: `args` --> src/lib.rs:72:25 | 15 | let args: Vec<String> = std::env::args().collect(); | ---- move occurs because `args` has type `Vec<String>`, which does not implement the `Copy` trait ... 71 | all_args: args, | ---- value moved here 72 | cmd_args: (&args[first_arg_index..]).to_vec().to_owned(), | ^^^^ value borrowed here after move
Here is my attempt to explain it.
all_args: args, cmd_args: (&args[first_arg_index..]).to_vec(),
The first line sets the all_args field of the struct to the args vector returned earlier from parsing the command line parameters. If I understand correctly, this takes ownership of the vector from the thing named args and gives it to the thing named all_args. At this point, args is no longer valid. If all_args is not in scope, then memory holding the vector may have been freed, and even otherwise, something might have changed its values through all_args, leaving args in an unreliable state.
The next clause tries to set a different field based on a slice of args, which is now invalid.
cmd_args: (&args[first_arg_index..]).to_vec(),
When I get something like this wrong, which is constantly, it is usually more complicated than this.
There is a chance that some syntax will let cmd_args initialize from all_args within WinkConfig, and the compiler even seems to have some intelligence about this, but I could not figure it out. Things like this give various exceptions.
cmd_args: (&self::all_args[first_arg_index..]).to_vec(),
In this case, a different solution is incredibly simple: create the slice before changing ownership, which means just reversing the order of these two statements so that args is still valid when creating the slice, before giving ownership of the vector to all_args.
cmd_args: (&args[first_arg_index..]).to_vec(), all_args: args,
We can create a similar compilation error by removing to_owned() from some of the other expressions, such as at the start of that last block.
cmd_name: args[0], // removed to_owned()
This will cause an exception because after exiting the function, args still has ownership of the data, but is out of scope and therefore invalid. I cannot explain why cmd_args gets ownership in the first case but cmd_name does not get ownership in this case; I assume it has something to do with args[0] being owned by the vector even after retrieving its value or maybe one case involves a reference and the other doesn’t or one piece of data is on the stack rather than the heap or something along those lines. By adding to_owned(), we give cmd_name ownership of that data, or make a copy, or something, because args[] also still seems to be able to use it.
error[E0507]: cannot move out of index of `Vec<String>` --> src/lib.rs:65:23 | 65 | cmd_name: args[0], | ^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait For more information about this error, try `rustc --explain E0507`.
Or this one just before that.
command_code = args[first_arg_index]; // removed to_owned()
Here is the result.
error[E0507]: cannot move out of index of `Vec<String>` --> src/lib.rs:60:28 | 60 | command_code = args[first_arg_index]; | ^^^^^^^^^^^^^^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait For more information about this error, try `rustc --explain E0507`.
Despite warnings due to the program not yet using this code, after re-ordering those two clauses and restoring the to_owned() function calls, this code now compiles, but has not been tested in the slightest. I am particularly concerned about the following line,s which probably use the wrong values and may cause panics or something if there are no such elements in the source vector.
if first_arg_index < args.len() ... cmd_args: (&args[first_arg_index..]).to_vec(),
One thought on “Simple Case and Solution for Borrowing a Moved Value in Rust”