-
Notifications
You must be signed in to change notification settings - Fork 260
[BUG] move-assignment operators defaulting to memberwise semantics makes it impossible to cleanup old value #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I wonder if the general structure of generated move-assignment operators should be something more like: auto Foo::operator=(Foo&& that) noexcept -> Foo& {
auto tmp {std::move(that)};
this->destructor_logic();
member1 = std::move(tmp.member1);
member2 = std::move(tmp.member2);
// ...
// insert given logic here
// ...
return *this;
} where Anecdotally, this would work for all the move-assignment operators I tend to write and would mean I could rely on the auto-generated move-assignment if I only wrote the Alternatively, it could be something like: auto Foo::operator=(Foo&& that) noexcept -> Foo& {
if (this != &that) {
this->destructor_logic();
member1 = std::move(that.member1);
member2 = std::move(that.member2);
}
// ...
// insert given logic here
// ...
return *this;
} |
Some background:
It's possible if you use an IIFE. See https://cpp2.godbolt.org/z/e5aEe14b6: Cpp2
Cpp1 auto ComPtr::operator=(ComPtr&& that) noexcept -> ComPtr& {
m_raw = [&]() -> auto{auto new_raw {std::exchange(std::move(that).m_raw, nullptr)}; if (m_raw) {CPP2_UFCS_0(Release, (*cpp2::assert_not_null(m_raw)));}return new_raw; }();
return *this;
// Take the raw pointer from `that` first.
// If `&that == &this`, setting it to nullptr
// first ensures we don't release it.
} Normally, that'd require captures. |
I suppose it is possible. But definitely unintuitive, and could be awkward if you're dealing with multiple member variables. I'd argue that doing something destructor-ish with the old value is the norm for custom move-assignment operators and you shouldn't have to jump through IIFE hoops to do it. This isn't the only case where you want to run code before memberwise assignment. Argument validation is another big one, and that'd be useful for constructors as well as assignment operators. |
It makes sense to lift the restriction that the memberwise assignments must come first. |
By writing separate construction and assignment, plus the new feature of suppressing assignment to a member by writing `member = _ ;` (now allowed only in assignment operators). I do realize that's an "opt-out" which I normally prefer to avoid, but: - I considered and decided against (for now) the alternative of not having assignment be memberwise by default. I want to keep the (new to Cpp2) default of memberwise semantics for assignment as with construction. I think that's a useful feature, and normally if you do assign to a member it doesn't arise, and so I think it makes sense to explicitly call out when we're choosing not to do any assignment at all to a member before doing other assignment processing. We'll get experience with how it goes. - `_` is arguably natural here, since it's pronounced "don't care." There too, we'll see if that is natural generalized, or feels strained. For now it feels natural to me.
From commit cdf71bd, generalizing
Actually, would simply using |
Thanks @jbatez ! Yes, the new
and I checked that the assignment operator now correctly lowers to this:
|
Regarding implementing the assignment operator in terms of destruction + re-construction... that's possible, but has some issues. The main one is that it could lead to observable difference in behavior between copy assignment and move assignment. We wouldn't want to implement copy assignment in terms of destroy+construct for efficiency (e.g., a For completeness: See also GotW #23, but your suggestion as written already avoids most of the pitfalls mentioned there, which is great. 👍 |
#518 should be closed, too. |
Thanks! I still find the opt-out nature of it unintuitive, though, fwiw. And the fact that |
The fact that there's magical invisible code that you have to act on is indeed One More Thing To Teach, instead of One Less Thing. |
Thanks @jbatez and @jcanizales, let me ask a clarifying question to make sure I understand what is surprising... given code like this:
and those four functions compiles to this
Is the concern the
... so I was concerned about it being inconsistent if the programmer got different defaults if they wrote assignment themselves. So to me the defaults seem the same... but I totally accept that to you the defaults seem different, and I'd like to understand that more. What you would like these four functions to do differently? I'm all ears! |
I think there's a bit of a disconnect or a jump in reasoning that wasn't written down, because @jbatez wasn't writing a converting assignment. In my case: If you tell me, in Cpp2 to get the default [regular / copy / move] constructor instead of Even less intuitive to me is "you can modify the invisible code by knowing what the code is and writing opt-outs for specific lines". I am aware that I'm not bringing an alternative solution. Now, re the question about the defaulted converting constructor, which I'm not sure how it's related. I don't think a default exists semantically for that case. An implementation that doesn't use the argument seems obviously wrong to me, a bug; and so I don't see the benefit of providing it. |
OTOH, if the restriction that "assignments must appear first" is lifted, like @JohelEGP mentions here, then I think the rule "If the body of an That still leaves the converting assignment buggy if the user doesn't write a body. But I think a compiler should be able to flag the unused argument? |
@hsutter
So the only difference between the two operators is
|
Thanks, let me noodle on these suggestions. Maybe it's time to pick up pursuing the init-before-use rather than init-first semantics. @gregmarr That's a good example too, and I should probably flag the case where the Again, thanks! |
Even though |
I think flagging |
Ah, I missed that this was referring to the same type. OK, I should flag that with a 'use move that instead.' Thanks for clarifying! |
I personally find that most the implicit behavior that's exclusive to
I could keep going, but the theme of all my complaints is the same. Getting rid of boilerplate is nice, but when the implied behavior is different from the rest of the language, I think it should be opt-in. My proposal:
|
Also require an `operator=` second parameter of the same type to be named `that` - see hsutter#475 discussion thread
Consider a smart pointer type for
IUnknown
that automatically callsIUnknown::Release
where appropriate:cppfront generates the following code for the move-assignment operator:
cppfront inserted
m_raw = std::move(that).m_raw;
before my code, meaning there's no way for me to callRelease()
on the old value.The text was updated successfully, but these errors were encountered: