70 %
Chris Biscardi

Matching on structopt subcommands and using values after move

Given the following [[structopt]] definition that includes a subcommand enum:

rust
#[derive(StructOpt, Debug)]
struct Opt {
#[structopt(subcommand)]
cmd: Command,
}
#[derive(StructOpt, Debug)]
enum Command {
Write {
#[structopt(short, long)]
title: Option<String>,
},
}

We can match on opt.cmd to determine which Command variants was called. This lets us specify the variants as match criteria and also destructure the values out to use them. We can match on opt.cmd to determine which Command is active and to pull out the values that can be provided.

rust
fn main() -> Result<()> {
let opt = Opt::from_args();
dbg!(&opt);
match opt.cmd {
Command::Write { title } => todo!(),
}
}

Ignoring fields in destructuring

As written, we run into two issues with this code. One is that we aren't using title yet. When destructuring, we can use the placeholder (_) in our pattern.

warning: unused variable: `title`
--> src/main.rs:32:26
|
32 | Command::Write { title } => todo!(),
| ^^^^^ help: try ignoring the field: `title: _`
|
= note: `#[warn(unused_variables)]` on by default
placeholder-in-pattern.rs
rust
fn main() -> Result<()> {
let opt = Opt::from_args();
dbg!(opt);
match opt.cmd {
Command::Write { title: _ } => todo!(),
}
}

value used here after move

The second issue is that the opt variable is moved before we try to destructure it. If we look at the compiler output before we fixed the title issue we see the value use after move pointing at title specifically.

error[E0382]: use of moved value
--> src/main.rs:32:26
|
29 | let opt = Opt::from_args();
| --- move occurs because `opt` has type `Opt`, which does not implement the `Copy` trait
30 | dbg!(opt);
| ---------- value moved here
31 | match opt.cmd {
32 | Command::Write { title } => todo!(),
| ^^^^^ value used here after move

If we look at it after fixing the unused title, we see the compiler point at opt.cmd.

error[E0382]: use of moved value: `opt.cmd`
--> src/main.rs:31:11
|
29 | let opt = Opt::from_args();
| --- move occurs because `opt` has type `Opt`, which does not implement the `Copy` trait
30 | dbg!(opt);
| ---------- value moved here
31 | match opt.cmd {
| ^^^^^^^ value used here after move

This is because opt is moved into the dbg call because it doesn't implement the Copy trait. Now, we could implement or derive Copy for Opt if Command can implement Copy, but we can not implement Copy for Command. This is because String does not, and can not, implement Copy, and we have a String in our title.

Without diving into the depths of Copy, Clone, and allocation, there is something we have that already implements Copy, so we don't need to.

Shared references implement Copy.

So instead of passing opt into dbg!, and thus using move semantics (which are the default in Rust), we can pass a shared reference in: dgb!(&opt) which lets us use copy semantics.

The difference between move semantics and copy semantics in this case is that we can access opt after passing it to dbg!.

Under the hood, both a copy and a move can result in bits being copied in memory, although this is sometimes optimized away. -- Copy

rust
fn main() -> Result<()> {
let opt = Opt::from_args();
dbg!(&opt);
match opt.cmd {
Command::Write { title: _ } => todo!(),
}
}