Downsides of omitting trivial destructor calls
Via the std-proposals mailing list.
Which of these two functions — foo
or bar
— do you expect to give better codegen?
struct Integer {
int value;
~Integer() {} // deliberately non-trivial
};
void foo(std::vector<int>& v) {
v.back() *= 0xDEADBEEF;
v.pop_back();
}
void bar(std::vector<Integer>& v) {
v.back().value *= 0xDEADBEEF;
v.pop_back();
}
Compile both with GCC and libstdc++. Did you guess correctly?
foo:
movq 8(%rdi), %rax
imull $-559038737, -4(%rax), %edx
subq $4, %rax
movl %edx, (%rax)
movq %rax, 8(%rdi)
ret
bar:
subq $4, 8(%rdi)
ret
What’s going on here is that GCC is smart enough to understand that when you
run a destructor on a piece of memory, you end its lifetime, which renders all
preceding writes to that piece of memory “dead”. But GCC is also smart enough
to understand that a trivial destructor (such as the pseudo-destructor ~int()
)
is a no-op with no effects whatsoever.
So, bar
calls pop_back
, which runs ~Integer()
, which marks vec.back()
as “dead”, and GCC eliminates the multiplication by 0xDEADBEEF
entirely.
On the other hand, foo
calls pop_back
, which runs the pseudo-destructor ~int()
(it might choose to omit the call altogether, but it doesn’t). GCC observes that
this is a no-op and forgets about it. Therefore GCC does not observe that vec.back()
is dead, and cannot eliminate the multiplication by 0xDEADBEEF
.
This happens for all trivial destructors, not just for pseudo-destructors like ~int()
.
Replace our ~Integer() {}
with ~Integer() = default;
and watch the imull
instruction
reappear!
UPDATE, March 2021: Trivially destructible objects’ lifetimes are now correctly ended by pseudo-destructor calls. This was one of the effects of Richard Smith’s P0593 “Implicit creation of objects for low-level object manipulation,” adopted into C++20 (but implemented by GCC 11 in all language modes, thank goodness).