Does idiomatic rust code always avoid 'unsafe'?

Viewed 185

I'm doing leetcode questions to get better at solving problems and expressing those solutions in rust, and I've come across a case where it feels like the most natural way of expressing my answer includes unsafe code. Here's what I wrote:

const _0: u8 = '0' as u8;
const _1: u8 = '1' as u8;

pub fn add_binary(a: String, b: String) -> String {
    let a = a.as_bytes();
    let b = b.as_bytes();
    let len = a.len().max(b.len());
    let mut result = vec![_0; len];
    let mut carry = 0;
    for i in 1..=len {
        if i <= a.len() && a[a.len() - i] == _1 {
            carry += 1
        }
        if i <= b.len() && b[b.len() - i] == _1 {
            carry += 1
        }
        if carry & 1 == 1 {
            result[len - i] = _1;
        }
        carry >>= 1;
    }
    if carry == 1 {
        result.insert(0, _1);
    }
    unsafe { String::from_utf8_unchecked(result) }
}

The only usage of unsafe is to do an unchecked conversion of a Vec<u8> to a String, and there is no possibility of causing undefined behaviour in this case because the Vec always just contains some sequence of two different valid ascii characters. I know I could easily do a checked cast and unwrap it, but that feels a bit silly because of how certain I am that the check can never fail. In idiomatic rust, is this bad practice? Should unsafe be unconditionally avoided unless the needed performance can't be achieved without it, or are there exceptions (possibly like this) where it's okay? At risk of making the question too opinionated, what are the rules that determine when it is and isn't okay to use unsafe?

1 Answers

You should avoid unsafe unless there are 2 situations:

  1. You are doing something which impossible to do in safe code e.g. FFI-calls. It is a main reason why unsafe ever exists.
  2. You proved using benchmarks that unsafe provide big speed-up and this code is bottleneck.

Your arguing

I know I could easily do a checked cast and unwrap it, but that feels a bit silly because of how certain I am that the check can never fail.

is valid about current version of your code but you would need to keep this unsafe in mind during all further development.

Unsafe greatly increase cognitive complexity of code. You cannot change any place in your function without keeping unsafe in mind, for example.

I doubt that utf8 validation adds more overhead than possible reallocation in result.insert(0, _1); in your code.

Other nitpicks:

  1. You should add a comment in unsafe section which explains why it is safe. It would make easier to read a code for a other people (or other you after a year of don't touching it).
  2. You could define your constants as const _0: u8 = b'0';
Related