Live data from Hacker News

The repercussions of missing an Ampersand in C++ and Rust

nablag.com

121–128 of 128 posts

Re: The repercussions of missing an Ampersand in C++ and Rust

#121
post #99

Earlier quoted context omitted.

It doesn't, but it does mandate that the object has some "empty state". If you have an empty state you might as well have a default constructor which initializes the object to that empty state.

moved-from objects are not in an empty state but in an unspecified state, they are only required to be destructible, every other operation can be disallowed. That is not a useful state for default construction. Thus being movable does not imply defaulting is any sort of good idea. The other way around makes more sense, but even then it is not systematic, if default construction is costly (allocation, syscall, …) then…

> moved-from objects are not in an empty state but in an unspecified state, they are only required to be destructible, every other operation can be disallowed. That is not a useful state for default construction. Thus being movable does not imply defaulting is any sort of good idea.

This is only true for standard library objects. The C++ standard specifies that e.g std::unordered_map will be in a "valid but unspecified state" when moved from. You can define your own classes to behave however you want, including defining a moved-from object to be identical to a default-constructed object.

Regardless, you're missing the point. Even if the standard specified that every moved-from object can only be destructed and every other use was UB, you still need the moved-from object to be in some empty state. Your move constructor and move operator= needs to put the moved-from object into some state where it doesn't own any resources but can be safely destructed. There's typically little reason to not make this "doesn't own any resources" state available through a default constructor.

> The other way around makes more sense, but even then it is not systematic, if default construction is costly (allocation, syscall, …) then you don’t want to do that for a moved-from object which will just be destroyed, which is the fate of most.

If your class can be in some "empty" state that doesn't own any resources and doesn't require syscalls to construct, you want this to be both the default constructed state and the moved-from state. Default-constructible objects end up getting default-constructed then overwritten all the time in C++, such as by the common pattern 'my_unordered_map["foo"] = MyClass(...)' which will first default-construct a value and then call its move operator=.

Re: The repercussions of missing an Ampersand in C++ and Rust

#122

Earlier quoted context omitted.

> The same difference as making pointers always non-nullable and reintroducing nullability via an optional wrapper only when semantically appropriate. Again, I don't see what this has to do with destructive moves. If you want a socket class that always refer to an open socket, you can already do that. Same for non-nullable pointer wrappers. Conversely, destructive moves don't prevent you from implementing a socket cl…

> Again, I don't see what this has to do with destructive moves. If you want a socket class that always refer to an open socket, you can already do that. Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive. > How is this supposed to work? The very point of your so…

> Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive.

No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation. That's why I said that destructive moves would only somewhat simplify the move operations, but not make a qualitative difference (in this area).

> The sentinel value need not be exposed in the public interface of the class, it can only be accessible via the customization point of the optional.

Since the optional would need to construct an instance with the sentinel value, I thought that the "sentinel" constructor must be public. However, you might be right that one could write a template specialization that contains the template argument as a friend class. In this case you could use a private constructor. Note that the destructor still has to handle the sentinel value... But I guess this is just something you have to accept.

Re: The repercussions of missing an Ampersand in C++ and Rust

#123

Earlier quoted context omitted.

> First, one shouldn't use a moved-from object in the first place (except for, maybe, reassigning it). It still requires you to come up with somethkng to do to the old value in the move constructor. What would you do in the ValidatedAddress case? Set a flag in the struct called “moved_from” and use that to throw an exception if it’s ever used? Wouldn’t it be nice if you just didn’t need to worry about it? > Second, w…

> Wouldn’t it be nice if you just didn’t need to worry about it? Do you worry about it? I mean, to begin with, do you purposely try to reuse objects that you explicitly moved? If you do, in the very least you can be lazy and reassign a newly constructed object right after you explicitly move its contents, but I don't see any reason that would justify such a thing. Can you point out what you feel is the scenario that…

[deleted]

Re: The repercussions of missing an Ampersand in C++ and Rust

#124

Earlier quoted context omitted.

> Again, I don't see what this has to do with destructive moves. If you want a socket class that always refer to an open socket, you can already do that. Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive. > How is this supposed to work? The very point of your so…

> Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive. No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation. That's why I said that destructive moves would only somewhat simpli…

> No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation.

The issue is that the "moved-from" state is exposed to the user when the moves are not destructive. The author of the class has to consider behavior for every method in sentinel state, even when it's just to assert that the state isn't sentinel or "lol it's UB". And the user has to be careful not to accidentally misuse an object in sentinel state. Just like how every time you touch a nullable pointer you have to consider if it can be null and what to do in that case. As long as the sentinel state is exposed at all (via non-destructive move), there is little gain in not providing full support for it. However, with destructive moves the sentinel value either doesn't exist at all or only exists completely internally as an optimization, and all this mental overhead disappears.

Re: The repercussions of missing an Ampersand in C++ and Rust

#125

Earlier quoted context omitted.

> Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive. No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation. That's why I said that destructive moves would only somewhat simpli…

> No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation. The issue is that the "moved-from" state is exposed to the user when the moves are not destructive. The author of the class has to consider behavior for every method in sentinel state, even when it's just to assert that the state isn't sentinel or "lol it's UB". And the user…

I see your point. Just a few things:

1. This is only relevant when using such class as a local variable. Member variables are typically not moved-from.

2. In my understanding the user has the freedom to specify what constitutes a "valid but unspecified state" and it would be perfectly ok to mandate that anything you can do with a moved-from object is to either destroy or reassign it.

3. The problems with the state of moved-from objects from the perspective of a library author could have been prevented simply by imposing stricter requirements in the standard (e.g. every usage except destruction, and possible reassignment, shall be UB).

4. With all the issues you've pointed out, it is still be perfectly possible and reasonable to design a socket class your way (= no closed socket state) in C++, yet somehow most people seem to prefer open() and close() methods instead of modelling the state with an optional. Even in the presence of destructive moves, I don't think that one way is necessarily better than the other and it is mostly a matter of culture and personal preference.

All the being said, I definitely agree that destructive moves are good thing, in particular if the compiler prevents you accidentally accessing moved-from objects (which is a mistake that is very easy to make in C++).

Re: The repercussions of missing an Ampersand in C++ and Rust

#126

Earlier quoted context omitted.

> No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation. The issue is that the "moved-from" state is exposed to the user when the moves are not destructive. The author of the class has to consider behavior for every method in sentinel state, even when it's just to assert that the state isn't sentinel or "lol it's UB". And the user…

I see your point. Just a few things: 1. This is only relevant when using such class as a local variable. Member variables are typically not moved-from. 2. In my understanding the user has the freedom to specify what constitutes a "valid but unspecified state" and it would be perfectly ok to mandate that anything you can do with a moved-from object is to either destroy or reassign it. 3. The problems with the state of…

Indeed, the "valid but unspecified state" refers only to some types defined in the he standard library. It essentially means that you can only call methods which have no preconditions and don't depend on what that state is, e.g. assignment or destruction, or something like string::clear or vstring::assign if you want defined outcomes. In general each type is free to guarantee whatever the author wants about the moved from state, e.g. moved-from std::unique_ptr is always null.

Re: The repercussions of missing an Ampersand in C++ and Rust

#127
post #17
post #6

> I was specifically inspired by a performance bug due to a typo. This mistake is the “value param” vs “reference param” where your function copies a value instead of passing it by reference because an ampersand (&) was missing ... This simple typo is easy to miss the difference between `const Data& d` and `const Data d` isn't accurately characterized as "a typo" -- it's a semantically significant difference in inten…

If the implications of a one char diff are this egregious that they’re considered obvious, maybe it should take less cognitive effort to spot this? CI and tooling are great, but would be far less necessary if it was more difficult to make this mistake in the first place.

if you are programming in C++ then you have opted-in to a set of syntax and semantic properties that are ancient and well-defined and core to the language. those properties include at a very basic level exactly the sigil under discussion here.

it is not productive or interesting to characterize this absolutely core property of the language as "a one char diff" that takes any kind of special cognitive effort to spot

Re: The repercussions of missing an Ampersand in C++ and Rust

#128
post #17

Earlier quoted context omitted.

If the implications of a one char diff are this egregious that they’re considered obvious, maybe it should take less cognitive effort to spot this? CI and tooling are great, but would be far less necessary if it was more difficult to make this mistake in the first place.

What do you suggest? Some kind of std::const_reference ? Clang-tidy is enough in addition to the reviews.

The easiest* solution would be to do what rust does. You need to use & on both sides and error out on mismatch. Eg.

fn foo(bar: &Bar) { ... }

bar(&Baz)

* This would be a breaking change, so a non-starter.

Post reply on HN