Hacker News new | ask | show | jobs
by gpderetta 80 days ago
First of all mem::transmute is like bit_cast (which works perfectly fine in constexpr context), not reinterpret cast.

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++.
1 comments

Surely "We have many different ways to do this, each with different rules" is exactly the point? C++ 20's std::bit_cast isn't necessarily constexpr by the way although it is for the trivial byte <-> boolean transmutation I mentioned here.

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.

bit_cast and reinterpret_cast do different things: one works at the value level, the second preserves address identity (and it is problematic from an aliasing point of view).

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?

Firstly, that's not a direct translation because you're making two variables and I made none at all. Rust's const is an actual constant, it's not an immutable variable. We have both, but they're different. The analogous Rust for your bit cast example would make two immutable variables that we promise have constant values, maybe:

    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'm using two variables because numeric literals have the wrong type and bit_cast rejects transmutations between differently sized types.

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.

> I could have written it as x = bit_cast<bool>(char{2}), but does it really make a difference?

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.

> https://cpp.godbolt.org/z/EYnWET8sT

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.

Is there a reason Rust would not (as it was done in the ‘good ole days’) index the table via pointer arithmetic from .data? Also, I’m assuming that because you are discussing new devs, that they are not making the implementation decision to place the table on the heap and using Rist’s subscript operator, which I would understand Rust not doing as default. I can not think of a reason that the table should ever be put on the stack for reading a single value, so that being the default seems an oddly pessimistic default. I could be missing something regarding how Rust handles literal data ‘written out’ into source though.