Const all the things?

const all the things?
.
Last week someone posted a /r/cpp thread titled
“Declaring all variables local to a function as const”:

Ok, so I’m an old-school C++ programmer using the language now since the early ’90s.
I’m a fan of const-correctness for function and member declarations, parameters, and
the like. Where, I believe, it actually matters.

Now we have a team member who has jumped on the “everything is const” bandwagon.
Every code review includes dozens of lines of local function variables now declared
const that litter the review.

Intellectually, I understand the (what I consider mostly insignificant) arguments
in favor of this practice, but in nearly 30 years I have never had a bug introduced
into my code because a local function variable was mutable when I didn’t expect it.
It does nothing for me to aid in code analysis or tracking. It has at most a tiny
impact on performance.

Maybe I’m just an old dog finally unable to learn a new trick. So, on this fine
Wednesday, who’s up for a religious war? What are y’all doing?

TLDR: I’m not putting const on all the things, either.

Read in full here:

This thread was posted by one of our members via one of our news source trackers.

1 Like

Corresponding tweet for this thread:

Share link for this tweet.

1 Like

This function is wrong. How do I know? It passes by const value.

True, this is why rust has a few more validations than just const, there is, of course, const, but there is also static, single-referenced, and multi-referenced, which does it correctly, unlike C++.

In Rust you can’t pass a “const” type into functions, nor static, those are things relating to the life of something, not it’s access in rust, where C++ just kind of squeezes both a ‘const’ lifetime and a multi-referenced together, which is why you can do very questionable things like having the auto plus(const std::string s, const std::string& t) nonsensical function head in C++. Such a thing is unrepresentable in rust as ‘const’ isn’t a concept on a type, just the lifetime of it.

(In short, const in rust is more like constexpr in C++, const in C++ would be more akin to the 'static lifetime in rust, and static in rust is similar to static in C++, statically instanced data, however in Rust it’s considered multi-referenced (since obviously it is) so if you want to mutate it you have to have some way to synchronize that access on it. The C++ thing like const Blah& is like &Blah in Rust and the Blah& in C++ is the &mut Blah in Rust, but even that isn’t quite right because lifetimes can encode even more information in rust, like ‘when’ it is allowed to be read or mutated safely, which you just cannot do in C++).

The code above is also wrong, because it passes t by non-const reference. If t were really an out-parameter, it would be passed by pointer: std::string *t. The most likely explanation is that the programmer meant to pass by const reference and just forgot the const.

No, that’s an entirely valid function signature in C++, the std::string plus(const std::string& s, std::string& t) signature can easily say something like “I’m going to append s onto t and as such t must exist, no null”, it’s saying that s is immutable, it won’t be mutated, but t might very well be mutated, and the caller must account for that since the language isn’t.

Const data members are never a good idea.

Also not possible to do in Rust, access patterns are on the functions and variables, not the data storage itself. C++'s reasoning about this is… wrong…

But you know a better way to tell that a private data member can’t change? There’s no public API for changing it!

How Rust does it in other words. ^.^

Some people hear “never changes” and think it sounds a bit like “const,” so they slap const on that data member; but you shouldn’t lose sight of the fact that the way we preserve invariants in C++ isn’t with const, it’s with private.

Sounds like they want Rusts model. :wink:

My point is that having a const data member does pessimize move-constructions — enough to make const data members a bad idea — but not as much as people often think.

In Rust a move will consume the original, it is no longer accessible after that point, ever, at compile-time (no runtime checks), yes the compiler can actually check that in ways that seem like magic initially but quickly incredibly obviously to a long time C++ dev.

And yes you can swap values too, but only if their structures are entirely single-access (thus inherently mutable) or otherwise verified as such (like RefCell or a Mutex or so depending).

And copies of non-trivial types just never magically appear in Rust like they do in C++, you must call clone() if you want a deep copy of something, that cost won’t just magically and unexpectedly appear like it does in C++ at times.

Just as it’s always a mistake to pass “by const value,” it’s also a mistake to return “by const value.”

Ehhh, this one is more questionable, it can entirely be legitimate in C++ because proxy objects and such constructs.

I wouldn’t go so far as to say I never mark local variables const; but as a general rule, I don’t. As in the polymorphic-data-member case above, I don’t need a keyword just to tell me that a variable isn’t modified during its lifetime. I can see that fact at a glance, because:

Wrong, const is good on local variables to help enforce that the programmer themselves don’t accidentally screw up by mutating something that they shouldn’t have. Doesn’t matter how good of a programmer you are because this does happen to everyone eventually. Just, and I quote, at a glance seeing these things means that you are trusting the programmer, yourself, an extremely fallible human no matter what you do, to not screw up, and humans screw up, no matter how good they are. This is why default immutable (again C++ is mixing up immutable and actually lifetime ‘constant’) is best, if you want to mutate something that should be made explicit.

For an actual compile-time constant, I’d certainly consider marking it constexpr:

In other words more of a static lifetime, but actually enforceable because C++'s const is broken. The author is conflating static lifetime “constness” with immutable “constness”, because C++ messes those up so badly, and so the author is seeing the static lifetime form of it rather than the immutable form of it.

Here implicit move means that the return statement treats hi as an rvalue; but because it was const-qualified, that rvalue is of type const std::string&&, and can’t be pilfered from. A copy is made.

C++ is not necessarily wrong in allowing this, but it’s not clear either (however the broken optimization is indeed wrong, but unavoidable because of backwards compat). In Rust you can absolutely move an immutable value out of a function to something that holds it mutably, but that’s allowed as ownership has changed and different owners can do different things. If you want it to remain completely immutable then you wrap it in a (zero cost) Cell type, meaning that the value will absolutely always remain immutable, they can read from it, but they can’t mutate it, the best they can do is entirely replace their own value held version of it (which will call the destructor on the old version), or just make the api of the object immutable accesses only, then they can’t go through that either.

The programmer might try to fix it by writing e->setName(std::move(fullName))… but guess what? fullName was const-qualified, so the move does nothing!

Where in Rust it will be moved, or it just won’t compile if there is any access of it after this point. In Rust it would be e.set_name(full_name); and you know it will be cheap, it will be moved, or it just won’t compile (assuming the internal implementation doesn’t call clone() on it or something, but that’s an implementation detail, nothing to do with the API).

With respect to NRVO, this const is harmless. But still, this const cannot increase your performance! All it can do is lurk, eager to decrease performance if you get careless.

However it can increase correctness, to help ensure the programmer doesn’t mess up and mutate something that they shouldn’t. The possible (honestly rare) performance loss is due to C++ mis-design issues.

Sadly Rust does not have NVRO yet (though looks like it’s incoming, hope it’s soon), but it does reliably move the same code without unnecessary copies (this is the same godbolt as in the article but with the rust code and its generated machine code in there too (as it would give a compile error if it were unable to).


In short, making things default immutable is best, but C++'s conflation of const as both immutable and of being static is wrong and pessimises optimizations that it otherwise wouldn’t have to if it separated those very different things.

2 Likes