… … … … Unqualified name lookup has been challenging in C++ since even before C++11. Overload resolution rules are so painful that it took me weeks to review a patch simply because I had to back out of trying to make sense of the rules in the standard. There's several slightly different definitions of initialization. If you really want to get in the weeds, starting playing around with std::launder and std::byte and strict aliasing rules and lifetime rules, and you'll yearn for the simplicity of Rust.
C++ is the absolute most complex of any of the languages whose specifications I have read, and that's before we get into the categories of things that the standard just gives up on.
Annotations like std::launder, lifetime manipulation, etc solve a class of problems that exist in every systems language. They inform the compiler of properties that cannot be known by analyzing the code. Rust isn't special in this regard, it has the same issues.
Without these features, we either relied on unofficial compiler-specific behavior or used unnecessarily conservative code that was safe but slower.
This is both fundamentally true and misleading. Rust has to solve the same issues but isn't obliged to make all the same bad choices to do that and so the results are much better.
For example C++ dare not perform compile time transmutations so, it just forbids them and a whole bunch of extra stuff landed to work around that, but in Rust they're actually fine and so you can just:
const FOO: bool = unsafe { core::mem::transmute::<i8, bool>(2) };
That blows up at compile time because we claimed the bit pattern for the integer 2 is a valid boolean and it isn't. If we choose instead 0 (or 1) this works and we get the expected false (or true) boolean instead of a compiler diagnostic.C++ could allow this but it doesn't, rather than figure out all the tricky edge cases they just said no, use this other new thing we made.
I am confused by this assertion. You can abuse the hell out of transformations in a constexpr context. The gap between what is possible at compile-time and run-time became vanishingly small a while ago.
I think your example is not illustrative in any case. Many C++ code bases work exactly like your example, enforced at compile-time. That this can be an issue is a hangover from retaining compatibility with C-style code which conflates comparison operators and cast operators. It is a choice.
C++ can enforce many type constraints beyond this at compile-time that Rust cannot, with zero effort or explicit type creation. No one should be passing ints around.
const bool z = (const bool)((int8_t)2);
Is perfectly valid C++.
int8_t x = 2;
bool y = *reinterpret_cast<bool *>(&x);
But reinterpret_cast isn't valid in a constexpr scope.std::bit_cast is
Second, this compiles just fine:
constexpr int ivalue = 1;
constexpr bool bvalue {ivalue};
This fails at compile time (invalid narrowing): constexpr int ivalue = 2;
constexpr bool bvalue {ivalue};
Note we don't need bit_cast for this example as int to bool conversions are allowed in C++.I see that C++ people were more comfortable with the "We have far too many ways to initialize things" examples of this problem but I think transmutation hits harder precisely because it sneaks up on you.
Not sure what any of this has to do with initialization though.
FWIW, the direct translation of your rust code is:
constexpr char y = 2;
constexpr bool x = std::bit_cast<bool>(y);
It fails on clang for y=2 and works for y=1, exactly like rust;GCC produces UB for y=2, I don't know if it is a GCC bug or the standard actually allows this form of UB to be ignored at contexpr time.
What is the rust equivalent of reinterpret_cast and does it work at constexpr time?
edit: I guess it would be an unsafe dereference of a casted pointer. Does it propagate constants?
static y: u8 = 2;
static x: bool = unsafe { core::mem::transmute(y) };
Of course this also won't compile, because the representation for 2 still isn't a boolean. If it did compile you'd also (by default) get angry warnings because it's bad style to give these lowercase names.I also don't know if you found a GCC bug but it seems likely from your description. I can't see a way to have UB, a runtime phenomenon, at compile time in C++ as the committee imagines their language. Of course "UB? In my lexer?" is an example of how the ISO document doesn't understand intention, but I'd be surprised if the committee would resolve a DR with "That's fine, UB at compile time is intentional".
I understand that "these are different things" followed by bafflegab is how C++ gets here but the whole point of this sub-thread is that Rust didn't do that, so in Rust these aren't "different things". They're both transmutation, they don't emit CPU instructions because they happen in the type system and the type system evaporates at runtime.
So this is an impedance mismatch, you've got Roman numerals and you can't see why metric units are a good idea, and I've got the positional notation and so it's obvious to me. I am not going to be able to explain why this is a good idea in your notation, the brilliance vanishes during translation.
I could have written it as x = bit_cast<bool>(char{2}), but does it really make a difference?
I don't know enough rust to know what's the difference between its const and c++ constexpr. It might not be a meaningful difference in C++.
> So this is an impedance mismatch, you've got Roman numerals and you can't see why metric units are a good idea, and I've got the positional notation and so it's obvious to me. I am not going to be able to explain why this is a good idea in your notation, the brilliance vanishes during translation.
There are plenty of rust users on HN that are capable of kind, constructive, and technically interesting conversations. Unfortunately there are a small few that will destroy any goodwill the rest of the community works hard to generate.
Not really, that's also a variable. We're running into concrete differences here, which is what I was gesturing at. In C++ you've got two different things, one old and one new, and the new one does some transmutations (and is usually constexpr) while the old one does others but isn't constexpr. It's not correct to say that reinterpret_cast isn't a transmutation, for example it's the recognised way to do the "I want either a pointer or an integer of the same size" trick in C++ which is exactly that. Let me briefly explain, as much to ensure it's clear in my head as yours:
In C++ we have an integer but sometimes we're hiding a pointer in there using reinterpret_cast, in Rust we have a pointer but sometimes we're hiding an integer in there using transmute [actually core::ptr::without_provenance but that's just a transmute with a safe API]. Of course the machine code emitted is identical, because types evaporate at compile time the CPU doesn't care whether this value in a register "is" a pointer or not.
Anyway, yes the issues are the same because ultimately the machines are the same, but it's not true that C++ solved these issues the only way they could be addressed, better is possible. And in fact it would surely be a disappointment if we couldn't do any better decades later. I hope that in twenty years the Rust successor is as much better.
I don't know a way to express actual constants in C++ either. If there isn't one yet maybe C++ 29 can introduce a stuttering type qualifier co_co_const to signify that they really mean constant this time. Because constexpr is a way to get an immutable variable (with guaranteed compile time initialization and some other constraints) and in C++ we're allowed to "cast away" the immutability, we can actually just modify that variable, something like this: https://cpp.godbolt.org/z/EYnWET8sT
In contrast it doesn't mean anything to modify a constant in either language, it's not a surprise that 5 += 2 doesn't compile and so likewise Rust's core::f32::consts::PI *= 2; won't compile, and if we made our own constants we can't change those either. We can write expressions where we call into existence a temporary with our constant value, and then we mutate the temporary, but the constant itself is of course unaffected if we do this.
This can be a perf footgun, you will see newcomers write Rust where they've got a huge constant (e.g a table of 1000 32-bit floating point numbers) and they write code which just indexes into the constant in various parts of their program, if the index values are known at compile time this just optimises to the relevant 32-bit floating point number, because duh, but if they aren't it's going to shove that entire table on your stack everywhere you do this, and that's almost certainly not what you intended. It's similar to how newcomers might accidentally incur copies they didn't mean in C++ because they forgot a reference.
const SOUP: [f32; 1000] = [ /* whatever */ ];
let foo = something(blah_blah, blah) * SOUP[4];
// The optimiser will see that SOUP[4] is exactly say 1.5_f32 so it'll just do
// the same as if we'd calculated something(blah_blah, blah) * 1.5_f32
However let foo = something(blah_blah, blah) * SOUP[blah];
Now blah is a variable, we might need any value from SOUP, the optimiser doesn't know what values it might have - so a temporary is conjured into existence, equivalent to: let tmp = SOUP;
let foo = something(blah_blah, blah) * tmp[blah];
You do presumably recognise that this now puts SOUP on the stack right? The temporary is equivalent, but without the explicitness.Now if you know what you're doing you would of course have one single place in your program where you do this:
static SOUP: [f32; 1000] = [ /* whatever */ ];
And now there's an immutable global named SOUP and like "the good ole days" we don't keep writing this huge data blob to the stack only to subsequently look at a single element. But that's not the thing the noob wrote so that's not what they get."Sufficiently smart compilers" are a very gradual miracle. In theory a compiler could realise this is a good idea, in practice today I doubt you will find such a compiler, so just write explicitly that you want a single variable if that's what you want.
I'm afraid that just C++ being C++ and you are deep into UB; you can't really modify a constexpr value at runtime, and if you cast away its constness with what is effectively a const cast you are on your own. This will print "0 3" which is obviously nonsense:
constexpr int x = 3;
((int&)x) = 0;
char y[x];
std::print("{}, {}",x, sizeof(y));
The output might change according to the compiler and optimization level.You can also move the problematic sequence into a constexpr function and invoke it in a constexpr context: the compiler will reject the cast now.
Enum constants can't become lvalues, so the following also won't compile:
enum : int { x = 3};
((int&)x) = 0;
So I guess that's closer to the rust meaning of constant.FWIW, notoriously you could modify numeric literals in early Fortrans and get into similar nonsense UB.
edit: in the end[1] it seems that you take exception with constexpr not being prvalues in C++. I guess it was found more convenient for them to have an address [1]. That doesn't make them less constant.
[1] or at least I think you do, it is not clear to me what you have been trying to claim in this discussion.
[2] C++ will materialize prvalues into temporaries when their address is needed (and give them an unique address), I guess it was thought to be wasteful for large constexpr objects, and avoids the rust pitfall you mentioned.
Of course, but the reason to even do this is only to illustrate that it's just another variable, nothing more. As much for myself as for you.
I've heard the stories about older languages but I assume that's not a thing on a modern Fortran and I'm sure we agree it's a bad idea.
The main thrust of this sub-thread was that languages can, and I believe Rust did, choose to solve the same issues but in a better way and so "This is too complicated in C++" doesn't translate to "It will be too complicated in every language". I think some C++ people have a version of the "End of History" nonsense, which is a nineteenth century idea. If you think noteworthy change happened after that and so history didn't end then hopefully you agree that makes no sense for general world history, and perhaps you can agree likewise C++ isn't the final apex of programming languages.
More often than not (except if you inherit codebases but clang has a modernize tool) most of the cruft is either avoidable or caught by analyzers. Not all.
But overall, I feel that C++ is still one of the most competitive languages if you use it as I said and with a sane build system and package manager.
https://tartanllama.xyz/posts/cpp-initialization-is-bonkers/
It's not insane, it's just ... melt-inducing.
But out here on this miserable old Earth I happen to think that Rust’s errors are pretty great. They’re usually catching things I didn’t actually intend to do, rather than preventing me from doing those things.
As it happens, you are replying to the person who made Rust's errors great! (it wasn't just them of course, but they did a lot of it)
How would dependencies work in this schism? E.g. if serde starts using named impls, do all dependencies have to use named impls?
Rust had a better start, not the least because it wasn’t designed on top of an existing language like C++ was, but who knows what it will look like in 30 years.
Articles discussions new features always have difficult syntax. There have been proposals like this going on from the start.
Fortunately the language team is cognizant of the syntax and usability issues with proposals. There have been a lot of proposals that started off as very unwieldy syntax but were iterated for years until becoming more ergonomic.
This feels like it should end up similar. The driving desire here is the removal of a restriction, so hopefully it ends up as an end user simplification rather than complication.
Both better and worse.
The current version of idiomatic C++ is much cleaner, more concise, and more powerful than the version of C++ you are familiar with. You don't need C-style macros anymore. The insane template metaprogramming hacks are gone. Some important things that were problematic to express in C++ (and other systems languages to be fair) are now fully defined e.g. std::launder. C++ now has expansive compile-time programming features, which is killer for high-performance systems code, and is more expressive than Rust in important ways.
The bad news is that this was all piled on top of and in addition to the famous legacy C++ mess for backward compatibility. If you are mixing and matching ancient C++ with modern C++, you are going to have a bad time. That's the worst of all worlds.
But if you are lucky enough to work with e.g. an idiomatic C++20 code base, it is a much simpler and better language than legacy C++. I've done a few major C++ version upgrades to code bases over the years; the refactored code base was always smaller, cleaner, safer, and easier to maintain than the old version.
You can mitigate it with some practices but that this is even necessary is a crime. Initialization is one of the most basic things in software development. How do you fuck it up so badly?
On a day to day basis it doesn’t cause me issues but it offends me just on principle.
Certainly initialization is the single most confusing feature in C++, I can give you that.
But still doable with s few patterns to remember. And warnings always max level.
I use CLion mostly but I never stop coming back to Emacs+LSP.
And yes, the analysis is quite competitive tbh. People often talk about this weird thing or the other in C++ but the experience is quite better than what the ISO standard strictly has to offer.
Eclipse is one of the rare software suites which didn't get slower as the tech evolves. Yes, it's probably heavier when compared to 20 years ago, but it starts pretty quickly and works snappily. I'm a happy camper.
If only the Go tools didn't get discontinued, but alas. KATE/BBEdit + Gopls is a pretty nifty combo on Linux/macOS.
How does the Rust language team weigh the benefits of solving user problems with new language features against the resulting increased complexity? When I learned Rust, I found it to be quite complex, but I also got real value from most of the complexity. But it keeps growing and I'm not always sure people working on the language consider the real cost to new and existing users when the set of "things you have to know to be competent in the language" grows.
Keep in mind that as time goes on, features being introduced will be more and more niche. If you could do things in a reasonable way without the new feature, the feature wouldn't be needed. That doesn't mean that everyone needs to learn about the feature, only the people that need that niche have to even know about it (as long as it is 1) it interacts reasonably with the rest of the language, 2) its syntax is reasonable in that it is either obvious what's going on or easy to google and memorable so that you don't have to look it up again and 3) it is uncommon enough that looking at a random library you won't be seeing it pop up).
Yes, it's called "C++".
It's interesting to note the discrepancy between replies to this blog here and, say, lobste.rs (which is neutral to it).
Here it's very concerned about complexity, while on lobsters it's mostly about needing this feature - yesterday.
When I used to write Scala, I accepted the fact that I don't have a background in type/set/etc. theory, and that there were some facets of the language that I'd probably never understand, and some code that others had written that I'd probably never understand.
With a language like Rust, I feel like we're getting there. Certain GAT syntxes sometimes take some time for me to wrap my head around when I encounter them. Rust feels like it shouldn't be a language where you need to have some serious credentials to be able to understand all its features and syntax.
On the other end we have Go, which was explicitly designed to be easy to learn (and, unrelatedly, I don't like for quite a few reasons). But I was hoping that we could have a middle ground here, and that Rust could be a fully-graspable systems-level language.
Then again, for more comparison, I haven't used C++ since before they added lambdas. I wonder if C++ has some hairy concepts and syntax today on par with Rust's more difficult parts.