

Should assignment affect `is_trivially_relocatable`? – Arthur O'Dwyer – Stuff mo...
source link: https://quuxplusone.github.io/blog/2024/01/02/bsl-vector-erase/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Should assignment affect is_trivially_relocatable
?
Consider the following piece of code that uses Bloomberg’s bsl::vector
:
#include <bsl_vector.h>
using namespace BloombergLP::bslmf;
struct T1 {
int i_;
T1(int i) : i_(i) {}
T1(const T1&) = default;
void operator=(const T1&) { puts("Assigned"); }
~T1() = default;
};
static_assert(!IsBitwiseMoveable<T1>::value);
int main() {
bsl::vector<T1> v = {1,2};
v.erase(v.begin());
}
bsl::vector::erase
uses T1::operator=
to shift element 2
leftward into
the position of element 1
(and then destroys the moved-from object in position 2
).
The program prints “Assigned.”
But that’s because type T1
is not trivially relocatable!
BSL calls the relevant trait
IsBitwiseMoveable
, notIsTriviallyRelocatable
; but that’s only because their codebase predates the C++11 meaning of “move.” Qt, a codebase of similar antiquity, upgraded their own terminology from “movable” to “relocatable” only in November 2020.I don’t give a Godbolt link here only because BSL isn’t available on Godbolt yet: #5933.
Let’s try the same thing with a trivially relocatable type T2
:
struct T2 : NestedTraitDeclaration<T2, IsBitwiseMoveable> {
int i_;
T2(int i) : i_(i) {}
T2(const T2&) = default;
void operator=(const T2&) { puts("Assigned"); }
~T2() = default;
};
static_assert(IsBitwiseMoveable<T2>::value);
int main() {
bsl::vector<T2> v = {1,2};
v.erase(v.begin());
}
Now BSL knows that elements of type T2
can be trivially (that is, bitwise)
relocated; so bsl::vector::erase
destroys element 1
and then uses memmove
to shift element 2
leftward into that vacant position.
The program prints nothing; T2::operator=
is never called.
Okay, what’s the moral of this story?
-
If you warrant a type as “trivially relocatable,” you must be prepared for library-writers not only to skip some construct/destroy pairs, but also to skip some assignment operations.
-
To put it from the library-writer’s point of view: Every trivially relocatable type has a “sane” assignment operator. Assigning a trivially relocatable type means no more or less than transferring its value. Library-writers can and do optimize based on this fact.
-
To put it from the P1144 compiler’s point of view: Suppose we have a type like
T1
that appears perfectly trivially relocatable except that it has a user-providedoperator=
. That user-provided assignment operator could do anything. We must consider the type non-trivially-relocatable. (Indeed,std::is_trivially_relocatable_v<T1>
is false, for the same reason thatbslmf::IsBitwiseMoveable<T1>
is false.)
Now, of course a type with a customized assignment operator can be explicitly warranted
as trivially relocatable; that’s exactly what we do in T2
(using BSL’s library-based opt-in).
Here’s the same example using Qt’s library syntax for the warrant (Godbolt):
#include <QList>
struct T2 {
int i_;
T2(int i) : i_(i) {}
T2(const T2&) = default;
void operator=(const T2&) { puts("Assigned"); }
~T2() = default;
};
Q_DECLARE_TYPEINFO(T2, Q_RELOCATABLE_TYPE);
static_assert(QTypeInfo<T2>::isRelocatable);
int main() {
QList<T2> v = {1,2,3};
v.erase(v.begin() + 1);
// does not print "Assigned"
}
And using P1144’s syntax for the warrant:
struct [[trivially_relocatable]] T2 {
int i_;
T2(int i) : i_(i) {}
T2(const T2&) = default;
void operator=(const T2&) { puts("Assigned"); }
~T2() = default;
};
static_assert(std::is_trivially_relocatable<T2>::value);
As of this writing, my libc++ fork follows BSL-and-Qt’s lead in optimizing vector::erase
for trivially relocatable types.
My newer libstdc++ fork does not, yet; but eventually it will. (Godbolt.)
“Wait, isn’t it technically non-conforming to use anything but assignment in vector::erase
, because
vector::erase
’s Complexity element specifically requires
the assignment operator of T
to be called a certain number of times?” — Well, yes, you’ve got me there,
for now. But we (STL) would really like it to be conforming, because we (BSL, Qt, Folly, …) already do it!
WG21 likes to say that C++ should “leave no room for a lower-level language”; it doesn’t really make sense
that a valuable optimization that every third-party library vendor already does should be forbidden
to std::vector
on a technicality.
My P3055R0 “Relax wording to permit relocation optimizations in the STL” (December 2023) aims to patch this hole. D3055R1 adds a few “stretch goal” patches on top of R0. Feedback welcome; send me an email!
Test yourself
-
As a type author: Suppose your type
Cat
has a defaulted copy constructor and defaulted destructor, but you rely on non-value-semantic side-effects ofCat::operator=
; your program will misbehave if an assignment operation is replaced with destroy-and-reconstruct. IsCat
“trivially relocatable”? -
As a type author: Your program’s correctness depends on the compiler’s never eliding assignments of
Cat
. Suppose you mark it with the attribute —struct [[trivially_relocatable]] Cat
— thus forcingis_trivially_relocatable_v<Cat>
to yieldtrue
. Is this “lying to the compiler”? Will your program behave correctly after that change? -
As the compiler (or the human reader): Suppose you see a type
struct Dog
(not marked with the attribute). All of its data members are trivially relocatable. Its destructor and move-constructor are defaulted. But its move-assignment operator is user-provided; you can’t tell exactly what it does. As the compiler (or the human reader), is it safe to assume thatDog
is trivially relocatable?
(No; yes; no; no.)
Recommend
-
9
Thoughts on “Does GPT-2 Know Your Phone Number?”
-
4
It’s not always obvious when tail-call optimization is allowed
-
14
decltype of a non-static member
-
10
A hole in Clang’s -Wsuggest-o...
-
8
Don’t blindly prefer emplace_back to push_back
-
3
#Boost #Cpp #CppNowWhen Should You Give Two Things...
-
5
std::span should have a converting c...
-
6
Should the compiler sometimes reject a [[trivially_relocatable]] warrant? ...
-
4
Most C++ constructors should be explicit
-
9
Polymorphic types aren’t trivially relocatable
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK