Method for safely moving all elements out of a generic array into a tuple with minimal overhead

旧巷老猫 提交于 2021-01-27 17:30:04

问题


In Rust, I want to move all the elements out of a generic fixed-width array so I may then move them individually. The elements may but don't necessarily implement Copy. I've come up with the following solution:

struct Vec3<T> {
    underlying_array: [T; 3]
}

impl<T> Vec3<T> {
    fn into_tuple(self) -> (T, T, T) {
        let result = (
            unsafe { mem::transmute_copy(&self.underlying_array[0]) },
            unsafe { mem::transmute_copy(&self.underlying_array[1]) },
            unsafe { mem::transmute_copy(&self.underlying_array[2]) },
        );
        mem::forget(self);
        result
    }
}

It seems to work, but I want to know if it's safe in the general case. Coming from C++, it's not generally safe to move objects by copying their bit patterns and bypassing the source object's destructor, which I think is essentially what I'm doing here. But in Rust, every type is movable (I think) and there's no way to customize move semantics (I think), so I can't think of any other way Rust would implement moving objects in the un-optimized case than a bitwise copy.

Is moving elements out of an array like this safe? Is there a more idiomatic way to do it? Is the Rust compiler smart enough to elide the actual bit copying transmute_copy does when possible, and if not, is there a faster way to do this?

I think this is not a duplicate of How do I move values out of an array? because in that example, the array elements aren't generic and don't implement Drop. The library in the accepted answer moves individual values out of the array one at a time while keeping the rest of the array usable. Since I want to move all the values at once and don't care about keeping the array, I think this case is different.


回答1:


Rust 1.36 and up

This can now be done in completely safe code:

impl<T> Vec3<T> {
    fn into_tuple(self) -> (T, T, T) {
        let [a, b, c] = self.underlying_array;
        (a, b, c)
    }
}

Previous versions

As you've mentioned and as discussed in How do I move values out of an array?, treating a generic type as a bag of bits can be very dangerous. Once we have copied those bits and they are "live", implementations of traits such as Drop can access the value when we least expect it.

That being said, your current code appears to be safe, but has needless flexibility. Using transmute or transmute_copy is The Big Hammer and is actually rarely needed. You don't want the ability to change the type of the value. Instead, use ptr::read.

It's conventional to expand unsafe blocks to cover the range of code that makes something safe and then to include a comment explaining why the block is actually safe. In this case, I'd expand it to cover the mem::forget; the returned expression has to come along for the ride too.

You will need to ensure that it's impossible for a panic to occur between the time that you've moved the first value out and when you forget the array. Otherwise your array will be half-initialized and you will trigger uninitialized behavior. In this case, I like your structure of writing a single statement that creates the resulting tuple; it's harder to accidentally shoehorn in extra code in there. This is also worth adding comments for.

use std::{mem, ptr};

struct Vec3<T> {
    underlying_array: [T; 3],
}

impl<T> Vec3<T> {
    fn into_tuple(self) -> (T, T, T) {
        // This is not safe because I copied it directly from Stack Overflow
        // without reading the prose associated with it that said I should 
        // write my own rationale for why this is safe.
        unsafe {
            let result = (
                ptr::read(&self.underlying_array[0]),
                ptr::read(&self.underlying_array[1]),
                ptr::read(&self.underlying_array[2]),
            );
            mem::forget(self);
            result
        }
    }
}

fn main() {}

every type is movable

Yes, I believe this to be correct. You can make certain values immovable though (search for "one specific case".

there's no way to customize move semantics

Yes, I believe this to be correct.

Is the Rust compiler smart enough to elide the actual bit copying

In general, I would trust the compiler to do so, yes. However, optimization is a tricky thing. Ultimately, only looking at assembly and profiling in your real case will tell you the truth.

is there a faster way to do this?

Not that I'm aware of.


I'd normally write such code as:

extern crate arrayvec;
extern crate itertools;

use arrayvec::ArrayVec;
use itertools::Itertools;

struct Vec3<T> {
    underlying_array: [T; 3],
}

impl<T> Vec3<T> {
    fn into_tuple(self) -> (T, T, T) {
        ArrayVec::from(self.underlying_array).into_iter().next_tuple().unwrap()
    }
}

Investigating the assembly for both implementations, the first one takes 25 x86_64 instructions and the second takes 69. Again, I'd rely on profiling to know which was actually faster, since more instructions doesn't necessarily mean slower.

See also:

  • How do I move values out of an array?
  • How do I move String values from an array to a tuple without copying?


来源:https://stackoverflow.com/questions/49348654/method-for-safely-moving-all-elements-out-of-a-generic-array-into-a-tuple-with-m

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!