Return slice (or array of unknown size) from match expression

251 Views Asked by At

I'm trying to implement the following idea:

let command = ...;

let request = match subcommand {
    Some(x) => [command, x as u8, (x >> 8) as u8],
    None => [command],
};

request should be either an array of 1 (in case there is no subcommand) or 3 (in case there is a 2-byte subcommand). Obviously this approach does not work because branches of match expressions "return" different types (either [u8;1] or [u8;3]);

My logical follow-up was to use slices:

let request: &[u8] = match subcommand {
    Some(x) => &[...],
    None => &[...],
};

That works great on constant values such as &[0, 0, 0], but when I'm trying to build arrays using my variables it fails with temporary value dropped while borrowed.

I got that in that case my reference is escaping to the superior code block, but is there a way around it (i.e some lifetime annotation?)

Creating the buffer beforehand and using slices does work but does not feel optimal.

UPD: that's the function I'm trying to debug:

// Read the data from the device.
async fn read<R>(&mut self, command: u8, subcommand: Option<u16>) -> Result<R, ChipError<E>>
where
    R: TryFrom<u16>,
{
    // If the caller has provided a subcommand, build the request with command and subcommand.
    // If there is no subcommand, use the plain command instead
    let request: &[u8] = match subcommand {
        Some(x) => &[command, x as u8, (x >> 8) as u8],
        None => &[command],
    };

    // Send the bytes to the device
    self.i2c.write(self.addr, &request).await?;

    // And read the response...
    let mut response = [0, 0];
    self.i2c
        .write_read(self.addr, &[commands::CONTROL], &mut response)
        .await?;

    match u16::from_le_bytes(response).try_into() {
        Ok(value) => Ok(value),
        Err(_) => Err(ChipError::Value),
    }
}

And that's the compiler output:

error[E0716]: temporary value dropped while borrowed
  --> xxxx/src/lib.rs:69:25
   |
68 |           let request: &[u8] = match subcommand {
   |  ______________________________-
69 | |             Some(x) => &[command, x as u8, (x >> 8) as u8],
   | |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
   | |                         |                                |
   | |                         |                                temporary value is freed at the end of this statement
   | |                         creates a temporary value which is freed while still in use
70 | |             None => &[command],
71 | |         };
   | |_________- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value
3

There are 3 best solutions below

3
On

There are multiple ways you can do this. Here are some suggestions.

  1. Just use a Vec (or Box<[u8]>). Yes, it is less efficient, but does it matter for you? it is likely to be the clearest solution.
  2. Use a stack-allocated vector, e.g. ArrayVec.
  3. If the type has a simple default value, have an array in the outer scope and initialize part of it, and return a slice to this part:
let mut scratch_space = [0; 3];
let request = match subcommand {
    Some(x) => {
        let data = &mut scratch_space[..3];
        data.copy_from_slice(&[command, x as u8, (x >> 8) as u8]);
        data
    },
    None => {
        scratch_space[0] = command;
        &scratch_space[..1]
    },
};
  1. If this is not possible, declare multiple such arrays and only initialize one. That is:
let scratch_space1;
let scratch_space2;
let request = match subcommand {
    Some(x) => {
        scratch_space1 = [command, x as u8, (x >> 8) as u8];
        &scratch_space1[..]
    },
    None => {
        scratch_space2 = [command];
        &scratch_space2[..]
    },
};
2
On

This doesn't directly answer the question in the title, but assuming your writer is std::io::write (I know it's i2c, but you should be able to adapt), I would make a simple enum, like so:

use std::io::{Write, Error};

enum Request {
    Command(u8),
    CommandWithSubcommand(u8, u16)
}

impl Request {
    fn write_to(&self, mut writer: impl Write) -> Result<(), Error> {
        match self {
            Self::Command(command) => {
                writer.write_all(&[*command])?
            }
            Self::CommandWithSubcommand(command, subcommand) => {
                let [b1, b2] = subcommand.to_le_bytes(); //maybe to_be_bytes()?
                writer.write_all(&[*command, b1, b2])?
            }
        };
        Ok(())
    }
}

...
let request = match subcommand {
    Some(subcommand) => {
        Request::CommandWithSubcommand(command, subcommand)
    }
    None => {
        Request::Command(command)
    }
};
request.write_to(your_std_writer)?;
...

Because we use the write_all() method separately for each branch, and they both return the same result type, we do not have to worry about lifetimes. This can be inelegant, but advantageous.

Technically you can just use separate write calls in match subcommand, but I suggest using enums to help reason about 1-of-N-known-implementation problems.

0
On

The two answers above mine list excellent solutions. As mentioned in their answers:

  1. Rust must know the size of the return value because it exists on the stack. Returning two differently sized values doesn't work because it's not an exact size.
  2. You can't return a reference to data that exists on the stack because that would cause a use after free. Therefore, you can't return a slice.

Here is another solution that does not allocate on the heap.

use either::Either;

fn split(yes: bool) -> Either<[u8; 2], [u8; 4]> {
    if yes {
        Either::Left([1, 2])
    } else {
        Either::Right([1, 2, 3, 4])
    }
}

Either is a useful sum type of any two types. As a sum type, the size is the larger of the two variants plus a discriminant. The total size of the return value is five bytes in this case because [u8; 4] is the larger of the two values and the discriminant is one byte. If you don't want to add another crate just for Either, you can simply write your own since there isn't any magic involved in the type.

pub enum Either<L, R> {
    Left(L),
    Right(R)
}