'Question about exception guarantees for custom operator=
I have a class used to work with disk data consisting of an enum class
and vector<char>
sized based on the enum. The enum is an invariant for each object and is therefor const with two possible values. Because it's an invariant, one must write a operator=
member function. My standard approach is this:
A& operator=(const A& rhs) {
if (this != &rhs)
{
std::destroy_at(this);
std::construct_at(this, rhs);
};
return *this;
};
This is does not fulfill the strict exception guarantee and it isn't clear to me if it even meets the minimal guarantee.
The approach I'm using both protects against self assignment and provides strict exception guarantees but is somewhat awkward, requires code tailored to the class and only works with simple invariants that are trivially destructable and standard containers that have specialized swap()
methods. Fortunately, these restrictions apply to most of my use cases.
Is there some generalization like copy and swap that works with classes like these?
Example code
#include <iostream>
#include <vector>
#include <memory>
enum class Mode { data_sector, data_fat };
struct DiskBuf {
const Mode mode;
std::vector<char> buf;
explicit DiskBuf(Mode mode) : mode(mode), buf(mode==Mode::data_sector ? 2048 : 8192) {}
DiskBuf operator=(const DiskBuf& rhs) {
auto buf_tmp = rhs.buf; // this may throw but maintains strict exception guarantee
this->buf.swap(buf_tmp);
std::construct_at(&this->mode, rhs.mode);
return *this;
}
};
int main()
{
DiskBuf a{Mode::data_sector}, b{Mode::data_fat};
std::cout << "a.mode: " << (int)a.mode << ", a.buf.size(): " << a.buf.size() << '\n';
std::cout << "b.mode: " << (int)b.mode << ", b.buf.size(): " << b.buf.size() << '\n';
a = b;
std::cout << "After a=b\n";
std::cout << "a.mode: " << (int)a.mode << ", a.buf.size(): " << a.buf.size() << '\n';
}
Solution 1:[1]
Your code is not undefined behavior, but it is nearly impossible to avoid undefined behavior from your code.
const
data in C++ is truly immutable. You are attempting to bypass this. Your bypass really doesn't work; any variable, name, pointer or reference referring to the old object before the destroy/construct will not refer to the new object.
This causes problems at destruction time (because the variable is destroyed; this is double-destruction).
There are exceptions for "creating an object in place of an old one" that do not cause this problem, but they explicity exclude cases where there is an actual const
member or object.
In practice, this is because the compiler is free to notice your const
enum field is initialized at one spot, and be absolutely certain that the value never changes. It can skip any reading of the value at any point in the future, so long as it can prove the const
object was initialized with a specific value.
Your destroy/construct breaks this assumption. The result is the compiler's axioms being violated, and your program could be pathologically insane.
The standard does not specify that accessing a newly constructed object in a place where an old one with const
in it is not allowed due to the above optimization; it simply states your program is exhibiting undefined behavior. This UB exists, however, to make such optimizations possible.
const
fields and const
objects have meaning to the C++ compiler. const
references .. don't, as you are free to const_cast
away the const
. But really const
fields and objects are not something you can work around by assinging over them.
If you want something immutable but replacable, do this:
struct DiskBuf;
enum class Mode { data_sector, data_fat };
struct DiskBufInternal {
friend struct DiskBuf;
Mode mode;
std::vector<char> buf;
explicit DiskBufInternal(Mode mode) :
mode(mode),
buf(mode==Mode::data_sector ? 2048 : 8192)
{}
DiskBufInternal operator=(const DiskBuf& rhs) = delete;
private:
struct secret_token { explicit secret_token(int); };
public:
explicit DiskBufInternal(secret_token, Mode mode, std::vector<char> b) :
mode(mode),
buf(std::move(b))
{}
};
struct DiscBuf {
std::unique_ptr<DiskBufInternal> pImpl;
explicit DiscBuf(Mode mode):
pImpl(std::make_unique<DiskBufInternal>(mode))
{}
DiscBuf& operator=(DiscBuf const& o)&{
if (!o.pImpl) {
pImpl = {}
return;
}
pImpl = std::make_unique<DiscBufInternal>(
DiscBufInternal::secret_token(0),
o.pImpl->mode,
o.pImpl->buf
);
return *this;
};
here we keep a unique_ptr
to our DiscBufInternal
.
Another option is a std::optional
, but that lacks the weak exception guarantee. Maybe that can be gotten via a no-throw move ctor on the DiscBufInternal
.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Yakk - Adam Nevraumont |