6 ms·
> are still more ergonomic and, ironically, safer than Rust's poorly-designed `mem::transmute()` API. Eh, I disagree. I find: let x: f32 = ...; let y:
by dbaupp 9y ago
> are still more ergonomic and, ironically, safer than Rust's poorly-designed `mem::transmute()` API.
Eh, I disagree. I find:
let x: f32 = ...;
let y: i32 = mem::transmute(x);
is nicer than
float x = ...;
int y;
memcpy(&y, &x, sizeof x);
And, in terms of safety, transmute manages the sizes of everything and makes sure they match. There's no risk of passing the wrong size, or having types with mismatched sizes (resulting in either "slicing", or buffer overflows).
The only way I can see transmute being more dangerous than the equivalent is that one can allow the types to be inferred, which can result in badness if the inferred type isn't what the programmer is expecting. This is especially dangerous when type involves a lifetime. (I suppose one could argue that it also makes this dangerous operation easier to do, since there's an easy-to-find function for it. But... this goes both ways, since it also stops people being tempted into using undefined pointer casting like ^(float ^)(x) (using ^ for * since HN likes italics too much).)
> Language-lawyering is quite the rabbit hole, BTW; for example, it was discovered a few years ago that the wording of the formal C++ memory model defined in the C++11 and later standards means all non-trivial C programs (technically-speaking, not according to any existing or sane implementation) invoke undefined behavior in C++. We'll be lucky if the bikeshedding over the fix for this gets resolved in time to make C++20.
I'm curious if you've got a defect report link or similar, because my reading of "6.8 Object Lifetime" in the C++17 draft implies that the lifetime has started for those int objects since the two conditions are both satisfied:
- "storage with the proper alignment and size for type T is obtained": the malloc'd pointer is fine.
- "if the object has non-vacuous initialization, its initialization is complete": ints have vacuous initialization.
But, I'm probably wrong, and would appreciate being corrected.
- smitherfield 9y ago> Eh, I disagree... The reason transmute is (particularly) unsafe is because it can be quite difficult to tell whether a variable is bound to a value, a (possibly implicitly-dereferenced) reference or a slice, and so, given transmute's implicit type inference, it's very easy to quietly get either the source or destination type very wrong. The size-checking is also I would argue useless bordering on worse than useless (false sense of security) since any two references or two slices are the same size, but are unlikely to actually be interconvertible, and it also fails to check alignment. C and C++ (particularly the latter) make it much more difficult to inadvertently cast between values and pointers, and the strict aliasing rules (aliasing almost always illegal except through `char`) are draconian enough to discourage the practice of type-punning-through-indirection (and ensuing alignment bugs) altogether. `memcpy` (and unions-as-implemented) is nice because it's explicit and it just works. > I'm curious if you've got a defect report link or similar I'm having a hard time finding the detailed original paper pointing it out, but see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0593r0.html http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p059...
- dbaupp 9y ago> The reason transmute is (particularly) unsafe is because it can be quite difficult to tell whether a variable is bound to a value, a (possibly implicitly-dereferenced) reference or a slice, and so, given transmute's implicit type inference, it's very easy to quietly get either the source or destination type very wrong. Ah, so just the type inference I mentioned? I agree, and certainly try to never let transmute infer when I use it. However, I don't think one can end up with implicit dereferences or wildly unexpected types since there always has to be some context: the return value of transmute is completely unconstrained, meaning there needs to be something that suggests the type. Additionally, C++ can suffer in a similar way (but not quite identical) due to auto: auto source = ...; T dest; memcpy(&dest, &source, sizeof source); Or, in extreme cases: `auto dest = ...;`. > The size-checking is also I would argue useless bordering on worse than useless (false sense of security) since any two references or two slices are the same size, but are unlikely to actually be interconvertible, and it also fails to check alignment. ... C-style memcpy is strictly worse than all of this. But yes, maybe a false sense of security. But that's a bit like arguing that a safety guard on a buzz-saw doesn't stop all problems, and so is pointless. I agree that failing to check alignment of pointer destinations is unfortunate, but it's only one of many many problems that can occur with `unsafe` and even `transmute` itself. However, it's worth nothing that differing alignment between values being transmuted is not a problem: transmute::<[u8; 4], u32>(byte_array) is fine, despite the byte array only having alignment 1. > C and C++ (particularly the latter) make it much more difficult to inadvertently cast between values and pointers, and the strict aliasing rules (aliasing almost always illegal except through `char`) are draconian enough to discourage the practice of type-punning-through-indirection (and ensuing alignment bugs) altogether. I don't agree: - there's little difference between C/C++ and Rust other than the inference thing. C++ has C-style casts, and reinterpret_cast. - strict aliasing ends up being commonly violated for transmute-style casts (it's just so temptingly easy, and there's nothing that actually stops it compiling), and `unsafe` in Rust is also a fairly major discouragement to doing bad things. > `memcpy` (and unions-as-implemented) is nice because it's explicit and it just works. transmute is only fractionally less explicit (and it's well known bad practice to let it be completely implicit), and also just works. > I'm having a hard time finding the detailed original paper pointing it out, but see ... Thanks; interesting!
- camgunz 9y agoI think the Rust version is a lot less clear. I don't intuitively know what "transmute" is; if I guess it means "mutate across something... here from a float to an int" I don't know how it knows I mean int, so I have to suppose there's something special in Rust that lets me assign some bytes to a scalar like this; I worry that it does a copy instead of just mutating the bytes of `i32`; finally I'm unclear whether or not this uses unsafe (I bet that it does though). In contrast I know what memory is and I know what copying is. This is maybe the core of what bothers me about Rust? There are good and valid arguments about C, but its core attraction is "everything is a number, memory is an array". I think Rust has achieved a great thing, but I wish it were like, 50x simpler. I would give up so many things for that.
- dbaupp 9y ago> I don't intuitively know what "transmute" is That's fair, but... it's easily resolved by using Rust/reading the documentation. Humans don't intuitively know what, say, 'float' in C means, either, but they learn it quickly. Additionally, the non-Rust meaning of "transmute" is pretty close to how it's used in Rust: "To change, transform or convert one thing to another, or from one state or form to another". There's definitely a lot of good in making things obvious to beginners to the language, but there's also always a general collection of "jargon"/symbols one ends up having to learn. Someone's who's a beginner in Rust but familiar with C may spend more time working out what the transmute line means than the C, but someone familiar with Rust wouldn't (and certainly not if they're not familiar with C: they'd have to work out the src/dst order for memcpy). > I think Rust has achieved a great thing, but I wish it were like, 50x simpler As others have pointed out elsewhere many times: what would you remove? The core of what people complain about in Rust (lifetimes etc.), is also core to achieving it's goals. All of those core features are pretty orthogonal and fairly minimal.
- camgunz 9y agoSo I looked up transmute, and it turns out this is how it's called: let x: f32 = 0; let y: i32 = std::mem::transmute::<f32, i32>(x) I'm fine with this, actually. With the type information it's clear. I do think it's a little long-winded, but I don't see a way to shorten it without giving up namespacing or safety. > As others have pointed out elsewhere many times: what would you remove? The core of what people complain about in Rust (lifetimes etc.), is also core to achieving it's goals. All of those core features are pretty orthogonal and fairly minimal. I'm pretty OK with lifetimes. I think probably they could be a little more explicit and have better syntax, but I think they're complicated by nature and -- as you point out -- core to achieving memory/data race safety in Rust. I'm happy, however, to create a big gripe list here for you, haha :) Mostly my criticisms are: - some concepts are unclear because they use overloaded keywords or imprecise language - some features are confusing due to inconsistent structure/syntax - some (many) features aren't worth their complexity Innnnnnn order of the Rust book: Rust is often different for no real reason. Casting is a good example of this; what was wrong with `(i8)thing`? What does `as` gain us? Couldn't we have used `as` in a more powerful way, like context managers in Python for example? I'm not wild about `macro!`. Because macros are hygenic, I don't care if something is a macro or not and the `!` makes those calls stick out unnecessarily. I would have preferred that `!` indicated mutation in some way, like in Scheme for example. Or maybe get rid of the `mut` keyword and use `!` in variables. Really anything would be better. I don't find "everything is an expression" to be that valuable, and it leads to a lot of weirdness. Semicolons suppressing block expression values is one -- why would you ever want to use a block as an rvalue to only assign `unit` to the lvalue? Returning from loops is another, and that looks very weird: `break <expr>`. I can see how if/else as expressions is nice shorthand in a ternary kind of way, but I wouldn't (and don't, in any other systems language) mind just initializing my variables with zero values and updating them inside those blocks. Generally I like destructuring, but I think Rust's struct destructuring is a little much. I wouldn't mind having... I don't know 4 more lines of variable initialization the 0.0008% of the time I need to do this. I would prefer that ranges weren't special syntax and instead worked just like any other iterator. This also avoids weird parens in range-based pipelines. I'm a little on the fence with `match`. I think it strikes the right balance between Python's "use if/else for everything" and C's restrictive/dangerous switch, but most match blocks I see are messy. I think all in all I'm into it, and I like that it's got a history in other languages like OCaml, I just wish I could come up w/ a way to clean it up a little. match guards make me crazy though; that's exactly what if statements are for. The whole reason I like match/switch more than if/else is that it's restricted to the thing you're matching/switching on. A match guard can run a conditional on anything. It's completely superfluous if your language has if/else. You can probably guess I don't like match binding either. First I think that's what `let` is for, but I also think using `@` is both very non-intuitive and a big waste of an operator -- all to avoid a single `let` expression. if/while let I really like, but its weird that they don't follow the same rules as regular let expressions, like can `Some(i)` be an lvalue normally (no, it can't). I would prefer that they did follow the same rules, or that they used different syntax than `let` because they are in fact different. I wish closures taking no arguments didn't look like the boolean or operator. Using `cfn()` (closure function) here wouldn't have been terrible, I don't think. `pub` doesn't really mean "public", it means "visibility" and pub without modifiers means "visibility(public)". I do sort of like `mod`, but I dislike having to nest things inside of it. I would prefer a file-based approach, where you declared the file's module at the top. I would prefer `impl <trait> on <struct>` instead of `impl <trait> for <struct>` because `for` already means something else. I would have liked to use traits as types in function signatures instead of the clunky generic syntax. Alternatively just require the `where` clause. Both options reduce the number of things you have to know. I wish the "new type idiom" didn't reuse `struct`. Probably `type` goes there. I don't really like the ceremony around heap allocation. I know there are a lot of benefits to `Box`, but I'd prefer `*` to Box<T> and `alloc` or even just `new` to `Box::new`. Or hey, if you're against operator reuse, let's use `@` now that we've tossed match binding. Mainly my complaint is this is unnecessarily different from other systems languages. --- Probably these seem like really small issues, but I really think that all these things together would make Rust much more consistent and clear at a very slight cost to ergonomics.