Consider the following definition of Foo
:
struct Foo {
uint64_t data;
};
Now, consider the following definition of Bar
, which has the same data member as Foo
, but has an empty user-declared destructor:
struct Bar {
~Bar(){} // <-- empty user-declared dtor
uint64_t data;
};
Using gcc 8.2 with -O2
, the function copy_foo()
:
void copy_foo(const Foo* src, Foo* dst, size_t len) {
std::copy(src, src + len, dst);
}
results in the following assembly code:
copy_foo(Foo const*, Foo*, size_t):
salq $3, %rdx
movq %rsi, %rax
je .L1
movq %rdi, %rsi
movq %rax, %rdi
jmp memmove
.L1:
ret
The assembly code above calls memmove()
in order to perform the copy of the contiguous Foo
objects. However, the function below, copy_bar()
, which does exactly the same as copy_foo()
, but for Bar
objects:
void copy_bar(const Bar* src, Bar* dst, size_t len) {
std::copy(src, src + len, dst);
}
generates the following assembly code:
copy_bar(Bar const*, Bar*, size_t):
salq $3, %rdx
movq %rdx, %rcx
sarq $3, %rcx
testq %rdx, %rdx
jle .L4
xorl %eax, %eax
.L6:
movq (%rdi,%rax,8), %rdx
movq %rdx, (%rsi,%rax,8)
addq $1, %rax
movq %rcx, %rdx
subq %rax, %rdx
testq %rdx, %rdx
jg .L6
.L4:
ret
This assembly code doesn't call memmove()
, but performs the copy by itself.
Of course, if Bar
is instead defined as:
struct Bar {
~Bar() = default; // defaulted dtor
uint64_t data;
};
Then, both functions result in identical assembly code, since Foo
also has a defaulted destructor.
Is there any reason why user-declaring an empty destructor in a class prevents the compiler from generating a call to memmove()
to copy contiguous objects of that class?
std::memmove
can only be used on objects which are TriviallyCopyable, which requires a trivial destructor. Trivial destructors require that the destructor is not user-provided.
In your code for Bar
:
struct Bar { ~Bar(){} // <-- empty user-declared dtor uint64_t data; };
The destructor is user-provided, so Bar
is not TriviallyCopyable. Thus it would be incorrect in general for the compiler to generate a call to std::memmove
.
By the as-if rule, the compiler could theoretically detect that the destructor is empty and thus equivalent to being trivial, but it's apparent that this optimization is not included in the implementation of std::copy
.
The implementation of std::copy
from libstdc++ uses the equivalent of std::is_trivially_copyable
which is defined to report Bar
as not trivially copyable. Enabling this optimization would require libstdc++ to have a special type trait to detect this special case which is trivially avoidable by writing ~Bar() = default;
When you declare your own destructor that class is no longer trivially destructable nor trivially copyable. std::memmove
requires that the object passed to is trivially copyable so it can't be used on the class anymore.
The standard doesn't pose a requirement on the implementation to check and see if your destructor is actually non trivial, it just defaults to all user defined destructors being non trivial.
If your destructor is truly trivial then there is no reason to write one.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With