Live data from Hacker News

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

nablag.com

91–100 of 128 posts

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

#91
post #88

Earlier quoted context omitted.

In this case, I would want the address family and protocol to be statically known, so it would have default constructor. But for example, a file might not have one, sure. As for closing before lifetime ends, why? I can just end lifetime. Wrap it in an optional if the type system can't figure it out like with a struct member.

> so it would have default constructor. And what's the underlying value of such a default constructed socket? I assume it would be -1 resp. INVALID_SOCKET, in which case the destructor would have to deal with it. > Wrap it in an optional if the type system can't figure it out like with a struct member. So you essentially must wrap it in an optional if you want to use it as a member variable. I find this rather pointl…

> And what's the underlying value of such a default constructed socket? I assume it would be -1 resp. INVALID_SOCKET

No, as explained, the default value would be the result of `::socket` call, i.e. a fresh OS-level socket.

> So you essentially must wrap it in an optional if you want to use it as a member variable.

No, you only must wrap it if you really want this closed state to exist.

> Sure, you can implement a socket class like that, but it's neither necessary nor idiomatic C++.

Obviously. Because the moves are not destructive. If they were, this design would be superior. And the wasted space for optional is solvable, just like for non-nullable pointers.

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

#92
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…

Problem is it doesn't affect outcome at all unless you do mutation, and as such testing is irrelevant, but still can significantly impacts perf, and performance problems can take a while to surface; like, it may slowly grow from 0.1% of runtime to like 2%, low enough to not get get noticed at all at first, and still be too low to have significant thought put into it afterwards (but still way too high from a single missing character).

And, as you said, this is a meaningful difference in intent, so linting can't just blanket complain on every single instance of a non-&-ed argument.

And the difference in writing down intent is the wrong direction - doing a full nested object clone should require adding code in any sane language, whereas, in C++, making code clone takes.. negative one characters.

Whereas in Rust, the only thing that's ever implicit is a bitwise copy on objects with constant size; everything else requires either adding &-s or .clone()s, or your code won't compile.

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

#93
post #87

Earlier quoted context omitted.

> The destructor already has to deal with that. That's partly true, partly circular. Because moves work this way, it's harder to make a class that doesn't have empty states, so I don't design my class to avoid empty states, so the destructor has to handle them.

It's not like it's the only part of the language that mandates a default constructor though. There are plenty of situations where default-constructible types are desirable. Even simple things like having a non-default-constructible type in a map is awkward.

> It's not like it's the only part of the language that mandates a default constructor though

It’s… not a part of the langage which mandates a default ctor in the first place.

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

#94

> Granted, these repercussions of these defaults also result in (in my opinion) verbose language constructs like iter, into_iter, iter_mut ↩ Note that assuming the into_iter comes from IntoIterator that’s what the for loop invokes to get an iterator from an iterable. So for lr in LoadRequests.into_iter() { Is completely unnecessary verbosity, for lr in LoadRequests { Will do the exact same thing. And the stdlib will…

This is where I think linters can shine as educational tools. Underline either as an error and you’ve taught someone something that’s actually quite tricky to discover on your own. Similar to all the times I defensively str(something) in Python to find that “oh that has __str__ called on it anyways.”

When I was starting out in rust, replacing my IDE’s `cargo check` invocation with pedantic clippy (which has a lint for this use of `into_iter` [0]) was very useful in learning these parts of the language.

[0]: https://rust-lang.github.io/rust-clippy/master/index.html#ex...

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

#95

This might be an unpopular opinion - I think const by-value parameters in C++ shouldn’t exist. Const reference and mutable values are enough for 99% cases, and the other 1% is r-value refs. Regarding const by-value parameters, they should never appear in function declarations (without definition) since that doesn’t enforce anything. In function definitions, you can use const refs (which have lifetime extension) to ac…

Absolutely correct. Basically, C++ has value semantics — you pass arguments of type X like `void f(X x)`, and you return them like `X f()`, and that's good enough for a first approximation. (This is the only thing C lets you do.)

The second refinement is that you can use `const X&` as an optimization of `X`. (Perfectly safe for parameters; somewhat treacherous for return values.) Passing by `X&` without the const, or by `const X` without the ampersand, are both typos, and you should regularly use tooling to find and fix that kind of typo.

https://quuxplusone.github.io/blog/2019/01/03/const-is-a-con...

And that's it, for business-logic code. If you're writing your own resource-management type, you'll need to know about `X(X&&)` and `X& operator=(X&&)`, but ordinary business-logic code never does.

"What about `X&` for out-parameters?" Pass out-parameters by pointer. It's important and helpful to indicate their out-parameter-ness at the call-site, which is exactly what passing by pointer does. (And the pointer value itself will be passed by value, just like in C.)

"What about return by const value, like Scott Meyers recommended 20–30 years ago?" No, don't do that. It disables the ability to move-assign or move-construct from the return value, which means it's a pessimization. Scott found this out, retracted that advice in 2009, and correctly issued the opposite advice in his 2014 book.

https://quuxplusone.github.io/blog/2019/01/03/const-is-a-con...

At work I use a Clang patched with "-Wqual-class-return-type" to report return-by-const-value typos — since, again, `const X getter()` is almost always a typo for `const X& getter()`.

You can use that compiler too: https://godbolt.org/z/7177MTfb8

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

#96
post #89

Earlier quoted context omitted.

How would I use such a socket class as a member variable? How do I reopen the socket?

Reopen by constructing and assigning a new socket.

So I essentially have to wrap it in something like std::optional. Well, that's certainly one way to write a socket class, but I'd say it's not idiomatic C++. (I have never seen a socket class being implemented like that.)

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

#97
post #77

Earlier quoted context omitted.

How would you fix this in C++?

By adding syntax and semantics for destructible moves, meaning the moved object is removed from its scope (without calling its destructor.)

I've worked with C++ for a number of years, with a few codebases that were >1M LoC. Never did I stumbled upon a situation where an object was moved and an existing symbol became a problem. I wonder what you are doing to get yourself in that situation.

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

#99
post #87

Earlier quoted context omitted.

It's not like it's the only part of the language that mandates a default constructor though. There are plenty of situations where default-constructible types are desirable. Even simple things like having a non-default-constructible type in a map is awkward.

> It's not like it's the only part of the language that mandates a default constructor though It’s… not a part of the langage which mandates a default ctor in the first place.

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.

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

#100
post #91

Earlier quoted context omitted.

> so it would have default constructor. And what's the underlying value of such a default constructed socket? I assume it would be -1 resp. INVALID_SOCKET, in which case the destructor would have to deal with it. > Wrap it in an optional if the type system can't figure it out like with a struct member. So you essentially must wrap it in an optional if you want to use it as a member variable. I find this rather pointl…

> And what's the underlying value of such a default constructed socket? I assume it would be -1 resp. INVALID_SOCKET No, as explained, the default value would be the result of `::socket` call, i.e. a fresh OS-level socket. > So you essentially must wrap it in an optional if you want to use it as a member variable. No, you only must wrap it if you really want this closed state to exist. > Sure, you can implement a soc…

> If they were, this design would be superior.

I see how destructive moves would slightly simplify the implementation, but what difference would it make apart from that? (Don't get me wrong, I totally think that destructive moves are a good idea in general, I just don't see the qualitative difference in this particular case.)

> And the wasted space for optional is solvable, just like for non-nullable pointers.

In the case of non-nullable pointers the library author knows that they can use NULL as a sentinel value and write a corresponding specialization. But what could you possibly do with an arbitrary user-defined class?

Post reply on HN