In languages with well defined object lifetimes the destructors of by-value parameters have to be called at some point. This can be the responsibility of the caller or of the callee.
GCC and Clang compiling C++ for example decide to have the caller destroy the arguments. This can can lead to slightly inefficient code if the callee will always move the value out of the argument. For example after passing a std::unique_ptr
by value, the caller must always check if the value was moved out of the argument and if not call operator delete
on the pointer. Taking unique_ptr
by value and moving out unconditionally is a common idiom and the check in the caller is always unnecessary in this case.
Here
#include <memory>
// Will always move out of p
void foo(std::unique_ptr<int> p);
void bar() {
foo(std::make_unique<int>());
}
is lowered to
bar():
push rbx
mov edi, 4
sub rsp, 16
call operator new(unsigned long)
lea rdi, [rsp+8]
mov DWORD PTR [rax], 0
mov QWORD PTR [rsp+8], rax
call foo(std::unique_ptr<int, std::default_delete<int> >)
mov rdi, QWORD PTR [rsp+8]
test rdi, rdi ; Null pointer check here!
je .L1
mov esi, 4
call operator delete(void*, unsigned long)
.L1:
add rsp, 16
pop rbx
ret
mov rbx, rax
jmp .L3
bar() [clone .cold]:
Rustc on the other hand generates code where the callee destroys the arguments. So the unique_ptr
(or rather Box
) example does not require an extra null pointer check.
Here
#[inline(never)]
pub fn foo(p: Box<i32>) {/*...*/}
pub fn bar() {
foo(Box::new(0));
}
is lowered to
example::bar:
push rax
mov rax, qword ptr [rip + __rust_no_alloc_shim_is_unstable@GOTPCREL]
movzx eax, byte ptr [rax]
mov edi, 4
mov esi, 4
call qword ptr [rip + __rust_alloc@GOTPCREL]
test rax, rax
je .LBB1_1
mov dword ptr [rax], 0
mov rdi, rax
pop rax
jmp qword ptr [rip + example::foo@GOTPCREL] ; Tail call to foo, no cleanup afterwards
.LBB1_1:
mov edi, 4
mov esi, 4
call qword ptr [rip + alloc::alloc::handle_alloc_error@GOTPCREL]
ud2
So what are the advantages of the "caller destroys arguments" approach?
And, if there are others beside the one mentioned above, what are the advantages of Rustc's "callee destroys arguments" approach?
std::unique_ptr
and many other types the destruction involves a null pointer check. But if the compiler saw that the value was moved out it would statically know that the pointer is null and could elide the entire destructor. $\endgroup$