Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why this dead store of unique_ptr cannot be eliminated?

#include <memory>
#include <vector>
using namespace std;

vector<unique_ptr<int>> e;

void f(unique_ptr<int> u) {
    e.emplace_back(move(u));
}

For both Clang and GCC, the above code snippet generates something like:

f(std::unique_ptr<int, std::default_delete<int> >):
        mov     rsi, QWORD PTR e[rip+8] # rsi: vector.end_ptr
        cmp     rsi, QWORD PTR e[rip+16] # [e + rip + 16]: vector.storage_end_ptr
        je      .L52 # Slow path, need to reallocate
        mov     rax, QWORD PTR [rdi] # rax: unique_ptr<int> u
        add     rsi, 8               # end_ptr += 8
        mov     QWORD PTR [rdi], 0   # <==== Do we need to set the argument u to null here? 
        mov     QWORD PTR [rsi-8], rax # *(end_ptr - 8) = u 
        mov     QWORD PTR e[rip+8], rsi # update end_ptr
        ret
.L52:   # omitted

I was wondering why does the compiler generate mov QWORD PTR[rdi], 0 in this function? Is there any convention that requires compiler to do so?

Moreover, for simpler case like this:

void f(unique_ptr<int> u);

void h(int x) {
    auto p = make_unique<int>(x);
    f(move(p));
}

Why does the compiler generate:

call    operator delete(void*, unsigned long)

at the end of h(), given that p is always nullptr after the invocation of f?

like image 656
lz96 Avatar asked Dec 20 '25 00:12

lz96


1 Answers

In both of these cases, the answer is: because the object you moved from will still be destroyed.

If you look at the code generated for a call to

void f(unique_ptr<int> u);

you will notice that the caller creates the object for parameter u and calls its destructor afterwards as mandated by the calling convention. In case the call to f() is inlined, the compiler will most likely be able to optimize this away. But the code generated for f() has no control over the destructor of u and, thus, has to set the internal pointer of u to zero assuming that the destructor of u will run after the function returns.

In your second example, we have sort of the inverse situation:

void h(int x) {
    auto p = make_unique<int>(x);
    f(move(p));
}

Contrary to what the name may suggest, std::move() does not actually move an object. All it does is cast to an rvalue reference which allows the recipient of that reference to move from the object referred to—if he so choses. The actual move only happens, e.g., when another object is constructed from the given argument via a move constructor. Since the compiler does not know anything about what happens inside f() at the point of definition of h(), it can't assume that f() will always move from the given object. For example, f() could simply return or move only in some cases and not in others. Therefore, the compiler has to assume that the function might return without moving from the object and has to emit the delete for the destructor. The function could also perform a move assignment instead of a move construction, in which case the outer destructor would still be needed to release ownership of the object previously held by whatever was assigned ownership of the new object…

like image 145
Michael Kenzel Avatar answered Dec 22 '25 15:12

Michael Kenzel