Live data from Hacker News

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

nablag.com

11–20 of 128 posts

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

#11
Great article. It think it raises a good point. An important aspect of modern programming languages should be to simplify the syntax, to help developers avoid mistakes.

This reminds me of arguing more than once with JS developers about the dangers of loose typing (especially in the case of JS) and getting the inevitable reply ”I just keep track of my type casting.”.

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

#12

Note that taking a 'const' by-value parameter is very sensible in some cases, so it is not something that could be detected as a typo by the C++ compiler in general.

Yes. For example, if an argument fits into the size of a register, it's better to pass by value to avoid the extra indirection.

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

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

It's const so you're not changing it, and you're not sneaking a pointer either. So what's the difference in intent?

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

#14

> There are plenty of linters and tools to detect issues like this (ex: clang-tidy can scan for unnecessary value params) Exactly, this is not an issue in any reasonable setup because static analysis catches (and fixes!) this reliably. > but evidently these issues go unnoticed until a customer complains about it or someone actually bothers to profile the code. No

I think your estimate of how many C++ devs use linters is too high.

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

#15
post #4

Rust's behavior of moving without leaving a moved-out shell behind also simplifies the implementation of the type itself, because its dtor doesn't have to handle the special case of a moved-out shell, and the type doesn't even need to be able to represent a moved-out shell. For example, a moved-out-from tree in C++ could represent this by having its inner root pointer be nullptr, and then its dtor would have to check…

In practice, move operations typically just leave an empty object behind. The destructor already has to deal with that. And of course you can't call certain methods on an empty object. So in practice you don't need special logic except for the move operations themselves.

> 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.

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

#16

Note that taking a 'const' by-value parameter is very sensible in some cases, so it is not something that could be detected as a typo by the C++ compiler in general.

Right. Copying is very fast on modern CPUs, at least up to the size of a cache line. Especially if the data being copied was just created and is in the L1 cache.

If something is const, whether to pass it by reference or value is a decision the compiler should make. There's a size threshold, and it varies with the target hardware. It might be 2 bytes on an Arduino and 16 bytes on a machine with 128-bit arithmetic. Or even as big as a cache line. That optimization is reportedly made by the Rust compiler. It's an old optimization, first seen in Modula 1, which had strict enough semantics to make it work.

Rust can do this because the strict affine type model prohibits aliasing. So the program can't tell if it got the original or a copy for types that are Copy. C++ does not have strong enough assurances to make that a safe optimization. "-fstrict-aliasing" enables such optimizations, but the language does not actually validate that there is no aliasing.

If you are worried about this, you have either used a profiler to determine that there is a performance problem in a very heavily used inner loop, or you are wasting your time.

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

#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.

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

#19

> There are plenty of linters and tools to detect issues like this (ex: clang-tidy can scan for unnecessary value params) Exactly, this is not an issue in any reasonable setup because static analysis catches (and fixes!) this reliably. > but evidently these issues go unnoticed until a customer complains about it or someone actually bothers to profile the code. No

This is my gripe with C++ - I have to have a CI pipeline that runs a job with clang-tidy (which is slow), jobs with asan, memsan and tsan, each running the entire test-suite, and ideally also one job for clang and one for gcc to catch all compiler warnings, then finally a job that produces optimized binaries.

With Rust I have one job that runs tests and another that runs cargo build --release and I'm done...

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

#20
I like Rust's approach to this. It's even more important when comparing with languages that hide value/reference semantics at the call site.

I've been writing some Swift code in recent years. The most frequent source of bugs has been making incorrect assumptions on whether a parameter is a class or a struct (reference or value type). C# has the same issue.

It's just a terrible idea to make the value/reference distinction at the type level.

Post reply on HN