Exception safety of Vec::pop()

In rust Vec::pop() has the following signature:

pub fn pop(&mut self) -> Option<T>;

In c++ the same operation has to be done with two methods:

reference back();
void pop_back();

When reading up on the subject I encountered multiple explanations the most compelling one to me seemed to be exception safety. The following answer talks about front() and pop() but the discussion should be identical for back() and pop_back():

Consider this scenario (with a naive/made up pop implementation, to ilustrate my point):

template<class T>
class queue {
    T* elements;
    std::size_t top_position;
    // stuff here
    T pop()
    {
        auto x = elements[top_position];
        // TODO: call destructor for elements[top_position] here
        --top_position;  // alter queue state here
        return x;        // calls T(const T&) which may throw
    }

If the copy constructor of T throws on return, you have already altered the state of the queue (top_position in my naive implementation) and the element is removed from the queue (and not returned). For all intents and purposes (no matter how you catch the exception in client code) the element at the top of the queue is lost.

This implementation is also inefficient in the case when you do not need the popped value (i.e. it creates a copy of the element that nobody will use).

This can be implemented safely and efficiently, with two separate operations (void pop and const T& front()).

So now my questions is the following. Does rusts std::vec::Vec::pop() have this exception safety problem? I know that there isn't an exception mechanism in rust, but panics basically work the same way. But of course rust doesn't have a copy constructor concept. Vec::pop() simply reads the pointer value

    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            None
        } else {
            unsafe {
                self.len -= 1;
                Some(ptr::read(self.as_ptr().add(self.len())))
            }
        }
    }

So is it just that none of the operations in Vec::pop() panic and therefore the function is exception safe while c++ cannot assume anything about the copy constructor and therefore has to assume that it may panic?

Rust does not have this problem because there are no copy constructors. Moves are always just a memcpy, so pop can never panic.

8 Likes

Thanks for the confirmation :slight_smile:. Interestingly enough pop() does not perform a move but a ptr::read(). I couldn't yet figure out why but apparently it was decided it's better if the elements of a Vec get dropped in bulk instead of when popped.

What do you mean by dropped in bulk? There are no destructors in pop. The caller could drop it after pop returns if they want to.

1 Like

It does perform a move. It moves by using ptr::read.

3 Likes

The read is logically a move — one that leaves the vector element memory deinitialized rather than swapping something else in, which can't be done with regular Rust assignments via reference (a &mut must always point to a valid value).

The only case where the compiler automatically comprehends moving-out is local variables and Boxes; a Vec's memory is neither.

Ah yeah, sorry. I was confused because ptr::write() doesn't drop the underlying value. Potentially leaking memory. Of course in this case ptr::read() conceptually moves the value. Although confusingly enough the documentation for read says:

Reads the value from src without moving it. This leaves the memory in src unchanged.

According to this it should be possible to read a pointer twice, which would probably lead to UB if the pointer points to an object that handles memory resources, resulting in a double free.

There's no asymmetry here. ptr::write() may cause memory leakage by not running the destructor of the old value, and ptr::read() may cause a double-free by bitwise duplicating a value without invalidating the place.

However, pop() decreases the length of the vector, so it won't try to drop the logically-moved element in its own destructor, effectively giving up ownership.

2 Likes

Note that if you look at the internal details of Rust, x = read(p) actually ends up with the same MIR as x = *p. It's just that because it's unsafe it can do that without tripping the compiler error about copying potentially-not-Copy things from behind a pointers (as the programmer has to handle the lifetime management themselves).

Vec::pop is conceptually a move, implemented using a copy and a metadata update.

3 Likes