Currently in C++, if you write a struct which uniquely owns a resource, you have to write a lot of boilerplates in order to be "correct":
struct A {
A(A &&src) : ptr(src.ptr) {
src.ptr = nullptr;
}
A(const A &) = delete;
A &operator=(A &&src) {
delete ptr;
ptr = src.ptr;
src.ptr = nullptr;
return *this;
}
A &operator=(const A &) = delete;
~A() {
delete ptr;
}
private:
T *ptr;
};
And this is just one ptr, the required amount of duplicated efforts are correlated by the count of uniquely-owned resources in the wrapper type. Requiring the users to manually write all the assignments and then setting the source object back to null state can be tiresome and bug-prone. All the efforts went in, just to satisfy a niche scenario (use after move) which even static analyzers warn you against:
A a{};
auto b = std::move(a);
auto c = a; // Use after move, allowed by C++ standard (flagged by clang-tidy and cppcheck)
With relocation constructors, this is greatly simplified, because now there's less situations (moved-from state) to worry about:
struct A {
A(A) = default;
A(A &&) = delete;
A(const A &) = delete;
~A() {
delete ptr;
}
private:
T *ptr;
};
A a{};
auto b = reloc a;
auto c = a; // Error: use after relocate.
Even if the structure isn't trivially relocatable and if you're not working with move-only types, relocation constructor can still be significantly simpler than the old ways. This requires adding two new mechanisms (plus a syntax sugar):
1. Allow the "re-initialization" of relocated-from object:
auto a = 0;
auto b = reloc a; // `a` is now in relocated-from state.
a = 0; // `a` is re-initialized.
This does not conflict with the operator=(T) relocation assignment in the proposal. The re-initialization only occurs if the left operand is in relocated-from state.
This also doesn't break existing codes, because there are no relocated-from objects in any existing codes because reloc doesn't exist today.
2. When an object of a class type with no default constructor, is declared without initialization, then treat it as relocated-from state:
struct A {
A() = delete;
A(int);
};
auto f() {
A a; // `a` is considered to be relocated-from state
}
The idea comes from a "common sense" where:
is semantically equivalent to:
except it doesn't today, because class types with custom constructors have very different meaning:
int a;
// Fails to compile, because reference_wrapper
// doesn't have a constructor that takes no argument.
std::reference_wrapper<int> b;
b = a;
This mechanism would partially fix that:
int a;
// Can't be default constructed,
// therefore it's declared but in relocated-from state.
std::reference_wrapper<int> b;
b = a; // `b` is now initialized.
is now the same as std::reference_wrapper<int> b = a.
This doesn't break existing codes, because the above code snippet don't compile today due to unmatched constructor parameters. Those that compile today don't change semantic:
std::string a; // `a` is still default constructed because a default constructor can be found.
a = "test";
3. Destroy-then-re-initialize if no matching operator= is found:
struct A {
A(A);
A &operator=(const A &) = delete;
A &operator=(A) = delete;
}
auto f1() {
A a{};
A b{};
// attempts to match `operator=(A)` and `operator=(A &)` first,
// no matching overload found, then fall back to
// `reloc a` + `A(A)` (destory then re-initialize)
a = b;
}
It's bascially a syntax sugar for reloc left; left = right;.
This doesn't break existing codes, because the code doesn't compile today due to missing operator=.
With the new mechanisms in place, the following demonstrates how it simplifies libstdc++ style std::string SBO:
struct A {
A(std::span<T> input);
A(A src)
: storage(src.storage)
, ptr(src.is_small() ? &storage.buf : src.ptr)
{}
A(A &&) = delete;
A(const A &src)
: storage(src.storage)
, ptr(src.is_small()
? &storage.buf
: (T *) malloc(sizeof(T) * storage.capacity))
{}
~A() {
if(!is_small()) {
free(ptr);
}
}
private:
bool is_small() {
return ptr == &storage.buf;
}
union {
std::byte buf[16];
std::size_t capacity;
} storage;
T *ptr;
};
auto f1(std::span<char> data) {
auto a = A{data};
auto b = a; // A(const A &) called
auto c = reloc a; // A(A) called
A d; // uninitialized due to no matching constructors, same as relocated-from state.
//std::dump(d); // This will be a compile time error due to use after reloc.
d = reloc b; // initializes `d` with A(A), `d` is alive now.
d = c; // Due to no matching operator=, equivalent to `reloc d;` followed by `A(A)`.
return d; // NRVO, or A(A) called
}
vs the traditional way:
struct A {
A(std::span<T> input);
A(A &&src)
: storage(src.storage)
, ptr(src.is_small() ? &storage.buf : src.ptr)
{
src.ptr = &src.storage.buf;
}
A &operator=(A &&src) {
if (&src == this) {
return *this;
}
if(!is_small()) {
free(ptr);
}
storage = src.storage;
ptr = src.is_small() ? &storage.buf : src.ptr;
src.ptr = &src.storage.buf;
return *this;
}
A(const A &src)
: storage(src.storage)
, ptr(src.is_small()
? &storage.buf
: (T *) malloc(sizeof(T) * storage.capacity))
{}
A &operator=(const A &src) {
if (&src == this) {
return *this;
}
if(!is_small()) {
free(ptr);
}
storage = src.storage;
ptr = src.is_small()
? &storage.buf
: (T *) malloc(sizeof(T) * storage.capacity);
return *this;
}
~A() {
if(!is_small()) {
free(ptr);
}
}
private:
bool is_small() {
return ptr == &storage.buf;
}
union {
std::byte buf[16];
std::size_t capacity;
} storage;
T *ptr;
};
auto f1(std::span<char> data) {
auto a = A{data};
auto b = a; // A(const A &) called
auto c = std::move(a); // A(A &&) called
// A d; // It isn't possible to declare a variable without initializing it.
auto d = A{data};
d = std::move(b); // A(A &&) called
d = c; // A(const A &) called
return d; // NRVO, or A(A &&) called
}
All common use cases that originally required copy constructor + copy assignment + move constructor + move assignment, are now supported by just a relocation constructor + a copy constructor.
And not only it's simpler, it's also safer, because the users now repeat less than before.
Currently in C++, if you write a struct which uniquely owns a resource, you have to write a lot of boilerplates in order to be "correct":
And this is just one
ptr, the required amount of duplicated efforts are correlated by the count of uniquely-owned resources in the wrapper type. Requiring the users to manually write all the assignments and then setting the source object back to null state can be tiresome and bug-prone. All the efforts went in, just to satisfy a niche scenario (use after move) which even static analyzers warn you against:A a{}; auto b = std::move(a); auto c = a; // Use after move, allowed by C++ standard (flagged by clang-tidy and cppcheck)With relocation constructors, this is greatly simplified, because now there's less situations (moved-from state) to worry about:
A a{}; auto b = reloc a; auto c = a; // Error: use after relocate.Even if the structure isn't trivially relocatable and if you're not working with move-only types, relocation constructor can still be significantly simpler than the old ways. This requires adding two new mechanisms (plus a syntax sugar):
1. Allow the "re-initialization" of relocated-from object:
This does not conflict with the
operator=(T)relocation assignment in the proposal. The re-initialization only occurs if the left operand is in relocated-from state.This also doesn't break existing codes, because there are no relocated-from objects in any existing codes because
relocdoesn't exist today.2. When an object of a class type with no default constructor, is declared without initialization, then treat it as relocated-from state:
The idea comes from a "common sense" where:
is semantically equivalent to:
except it doesn't today, because class types with custom constructors have very different meaning:
This mechanism would partially fix that:
is now the same as
std::reference_wrapper<int> b = a.This doesn't break existing codes, because the above code snippet don't compile today due to unmatched constructor parameters. Those that compile today don't change semantic:
3. Destroy-then-re-initialize if no matching
operator=is found:It's bascially a syntax sugar for
reloc left; left = right;.This doesn't break existing codes, because the code doesn't compile today due to missing
operator=.With the new mechanisms in place, the following demonstrates how it simplifies libstdc++ style std::string SBO:
vs the traditional way:
All common use cases that originally required copy constructor + copy assignment + move constructor + move assignment, are now supported by just a relocation constructor + a copy constructor.
And not only it's simpler, it's also safer, because the users now repeat less than before.