I tended, in my C++ days, to think that if you need "reinterpret_cast", you're doing it wrong. Now that I code in safe Rust, I still think that.
If you really want to abstract this sort of thing, a good abstraction would be a write-only type, "space", representing memory with no content. Then make it possible to destroy an object and convert it to empty "space", or construct an object on empty "space". This creates a clear distinction between reusing memory and abusing the type system.
Such an object would be std::byte (and char), but it is still complicated when an (array of) this object is a member of another object. Nailing down the exact semantics has been very hard and compilers have been known to miscompile such code.
I think that the whole object model need to be formalized from scratch (similar to what was done for the concurrent MM) but that's still hard to do while preserving all the properties that current programs expect.
reinterprate_cast has it's place (mainly in the embdedded/kernel/low level software world) but unless you are in one of the fields where it is actually needed (aforementioned places where type punning is just nature of the beast) then there's really not a better alternative.
It is different though. Bit cast returns a new object, while reinterpret cast is in place. It doesn't matter for small objects, but it can matter for large ones.
If the compiler’s optimization passes determine that the bitcasted-from and bitcasted-to objects are never simultaneously live, then copying can be easily elided.
Usually, you either care about 0-copy, in which case you don't want to rely on "if the compile can tell", or you don't care about 0-copy, in which case it doesn't matter that the compiler did it.
reinterpret_cast guarantees that it is in-place, so it's the right tool for 0-copy.
You check today. What if tomorrow something changes? What if your network application starts dropping packets because it can't process them fast enough because an apparently unrelated change suddenly messed with a two year old optimization?
Then you fix it. Correct code is rarely hard to make faster.
What when your compiler starts to take advantage of latitude extended by the Standard, leading your program to produce sporadic, non-reproducible wrong results?
It is best not to code any UB. But it is often hard to be certain, around reinterpret_cast.
You rarely want to be spending time fiddling with the optimizer. Your code shouldn't rely on the optimizer to be efficient: don't use constructs whose semantics mean you are making a copy in places where you don't want a copy.
reintrepret_cast is hard to use correctly, but you only have to get it right once, and it has the right semantics, so no compiler changes will suddenly make it wrong or slow. memcpy and bit_cast have the wrong semantics, so they may change for unexpected reasons. In fact, it would be perfectly reasonable for a future version of the compiler to never optimize away the copy implied by memcpy and bit_cast.
Determining local variable lifetimes is a fundamental feature of a compiler. Literally any compiler that does register allocation can and will merge local variables that aren't simultaneously live. Because it turns out that the problem of fitting lots of variables into minimum registers is remarkably similar to the problem of fitting lots of variables into minimum registers + minimum stack space.
So yes, you can confidently rely on gcc or clang to merge local variables, even at -O1. If they suddenly don't do that after an update, it's a serious regression, and their mailing lists should be more than happy to receive that bug report.
For local variables. Once you're passing the data around to other functions, the problem becomes much more complex, and it becomes much less clear when and why the compiler will be able to understand what's happening.
Not to mention, in an example like processing packets, the two views of the data WILL be simultaneously live: you're taking raw bytes on one side, interpreting some part of them as a struct to decide what's happening next, then sending the raw bytes somewhere else.
EDIT: Basically the code would look something like this:
char* buf = new char[100];
read(buf, sizeof(header));
header *h = reinterpret_cast<header*>(buf); //assume we are compiling with "-fno-strict-alias", and that we know there are no alignment/packing concerns
get_destination(h).write(buf);
Also note that in C we have a somewhat cleaner solution:
The problem is that if you are bitcasting, say, from a memory buffer coming from a read() call it is very hard for the compiler to prove that bitcasted copy is the only live one, especially across function calls.
For small objects bitcast works just fine because the result end up in registers were you would have loaded the original objects anyway.
I think bit_cast may finally solve the last major use case of reinterpret_cast (building type safe memory mapped device objects) but I haven't sat down and tried to verify this yet.
Unfortunately there are also still a lot of platforms that have yet to get C++20 support and memcpy is not a viable alternative. Hopefully this is less of an issue in the future as major MCU orgs like TI have started switching to LLVM/Clang based compilers which can follow upstream with little to no changes needed.
Edit: I just did some digging and bit_cast does not work for the aforementioned use case since it can copy the data to a new location in memory. This prevents us from being able to reliably use it to write to memory registers.
reinterpret_cast comes up a lot when handling binary protocols - e.g. reading a message struct off a buffer / packet. How would this be handled in Rust or otherwise without copying the data?
You can always just create a helper type that wraps the byte range and provides getter/setter functions for the individual fields. There is no need to cast the thing to an actual struct.
In the medium term I'd say it's more likely you miss out on optimizations by not clearly expressing what you actually wanted here.
The more clarity is available as to what you meant, the more aggressive the optimiser can be in delivering precisely that. If you don't express exactly what is intended, the optimisers end up getting muzzled because the effect is so confusing.
What specific optimizations would a wrapper type allow that a accessing directly a pointer to a reinterpret casted, property aligned StandardLayout type would not allow?
The concern I have isn't about optimisations that are impossible but optimisations which are judged unwise and so either disabled by default or not implemented at all.
Maybe reinterpret_cast on StandardLayout with the correct alignment really does express exactly what we want here, I'm not sure. If so you're correct there wouldn't be a difference because a valid optimisation will always be exactly what everybody wanted. But if there are any other uses of these techniques the optimiser has to be pessimistic. That's why I prefer to be as explicit as possible.
This is how so many extra types of cast came into existence in C++ in the first place right, the recognition that casting could have different intents.
> StandardLayout with the correct alignment really does express exactly what we want here, I'm not sure
Surely if you are unsure the default position should be to go to the obvious and simple solution instead of overengineering it? But I might be completely misunderstanding your point.
[If the struct is not correctly aligned, I would either memcpy or apply any required compiler pragmas, while cursing the protocol designer]
In current Rust, you'd do it with an unsafe std::mem::transmute - possibly using a crate that wraps this call behind a safe interface that asserts that alignment and size are correct, and that your type doesn't have any padding, etc... See for instance the bytemuck crate.
In the future, rust might gain "Safe Transmutes", which would allow you to safely transmute from one type to another under some limited conditions. See https://github.com/rust-lang/lang-team/issues/21
The exact same approach as C is doing is possible in Rust. There are pointer casts and mem::transmute(). It does require unsafe, since you must be aware of gotchas with alignment, padding, and uninitialized memory.
If you want to be fancy, there's https://lib.rs/rkyv deserialization framework that will auto-generate appropriate zero-copy code for you.
There's also serde (de)serialization framework and bincode format, which aren't exactly zero-copy, but are fast 1-copy.
If you’re dealing with pure data, reinterpret_cast/mem::transmute is fine, and for certain types of low-level code, necessary. In Rust, I’ve recently discovered the “bytemuck” crate, which offers ways to do that safely.
"Pure data" has a very specific meaning there. "Bytemuck" uses the trait called "Pod", for "plain old data". What they mean is a structure for which all bit combinations are valid. This includes u8, u16, etc., and the floating point types, but excludes all reference types. "bool" is excluded (because it takes up 8 bits but only 2 of 256 values are valid), as are strings and chars, because they have to follow Unicode rules. And there must be no padding bytes in the structure.
For types with such attributes, coercions are safe. C++ could potentially have something like "reinterpret_cast" which worked only on such types. This would make the risky form of reinterpret_cast less necessary, and some projects might disallow it.
It's always hard to retrofit safety to a code base. You get a lot of pushback.
That mindset is what makes C++ and Rust both sometimes so annoying on embedded targets, if not actively harmful.
I've been left a strong impression that either I do it "safely" or I want to do it totally unsafe, there's often like no middle ground. No I don't want to suddenly accidentally double-dereference a pointer or cast away const, I just want to use this memory mapped peripheral. That is very unfortunate and short-sighted, IMO, considering that layer is where all safety and security begins from.
Do you find any issues with Rust that bothers you, as a former C++ developer?
I'm still in my C++ days, but I avoid reinterpret_cast, as well as RTTI and C++ exceptions. Looking forward trying out Rust.
The biggest headache I've hit is that you can't have back references. When A owns B, it's really hard to get from B to A. You have to reference count everything, even if B is a field of A.
A key concept in Rust is that no code can have access to two mutable references to the same object at the same time. That's the issue with std::launder, of course. The Rust
mechanism for doing it enforces stronger restrictions than absolutely necessary. Not that enforcing the minimal set of restrictions is easy.
The general argument of Rust enthusiasts is that you don't need that. But many of them are writing web back-end stuff, which does not have much state. I'm writing a client for a virtual world, which has an entire visible scene of constantly changing state. Representations for that sort of thing have extensive cross-links. Hammering that into the Rust model tends to result in things such as having to look up things in hashes where you'd use a direct link in C++.
What's desirable is safe single ownership with backlinks. That's hard. You need something like a weak reference to a singly owned object, along with the compile time checking needed to prevent its misuse. Right now, you can do that with reference counted weak links, which are checked at run time. But you're often calling ".borrow()" to follow a link, and that will fail if the mutable borrow count becomes > 1. If that check could be replaced with static analysis by the borrow checker, more safe data structures would be possible. Trees with backlinks, doubly linked lists, etc.
Not having this means you can reach the point where your program is one error away from compiling, and days of rewrite are needed to fix the problem. This is where unsafe code tends to sneak in, as an escape hatch.
I try to avoid circular references in C++ as well, at all cost, in my physics engines and game engines. For example, there is a unidirectional access from World -> Object -> Contact Point -> Vector3 and never the other way around (Vector3 doesn't know about Point, Object or World).
Would Rust allow to have all data in 'structs' (no private data), and make all logic pure functional (functions without side effects, data in, data out)? That way, you won't need circular references or stateful objects and it simplifies memory management.
Some parts of the system map well to a purely functional model, and some don't. The part that decodes incoming messages is almost entirely functional. The part that maintains the scene graph isn't. There's a continual stream of update messages coming in, requests for assets go out, and assets come back. One asset reply may update many objects that use the same mesh or texture, so there's not the one-to-one relationship that an "async" model wants.
Admittedly this is a somewhat unusual application. It's somewhere between writing an MMO client and a web browser. I once wrote a RSS feed reader in Rust, and that didn't have any of these hard problems.
I think someone should fork a new language from C++ and remove all the stuff that could be deprecated.
I don't mean to make a new language like Zig or rust or nim. I mean just a "saner" C++ without all the backward compatibility that is holding back the language.
Isn't part of the problem that no one can agree on the best subset? Compilers could provide pragmas and switches to enable projects to define their own language subset.
A fair point, but the major C++ compilers don't make much effort in letting you define a subset to permit.
For contrast, the Ada language (which is larger than C but smaller than C++) takes this idea quite seriously. There are various 'well known' subsets of Ada with interesting properties and tooling. These subsets are called profiles. There's SPARK, a very minimalist subset tailored for formal verification, and Ravenscar, for concurrent hard real-time systems. Profile compliance is checked by the compiler itself. (If I understand correctly, the profiles are defined using Ada's rough equivalent of #pragma in C and C++.)
In the C and C++ worlds there are subsets like Google's C++ style guide, LLVM's C++ style guide, MISRA C, and MISRA C++, but their tools (i.e. static analysis tools to check for compliance) are generally separate from the compiler, for what that's worth.
BetterC [0] is that, for me. It's a subset of D, that only relies on the C Standard Library. So all the features and syntax of D that is sane, without any of the extras that people find go too far beyond a system's language.
You still get all the compile-time constructions, RAII, memory protection, lambdas, modules and so on.
This idea reminds me of the Go+ language (https://goplus.org/), which is kind of what you're proposing. But instead of reducing the language, they just "added less"!
I think they were inspired by C++, but decided one "plus" to the language was enough. Maybe the "pluses" add exponential complexity, and C++ hasn't reached the end of its second plus stage yet. /jk
Well, there are almost no good enough alternative to C++, in my view. Rust is hard to learn, Zig and other change the syntax way too much.
Also, it would not be difficult to link against existing C++ libraries, or at least to wrap them using extern C.
The goal would be a language that has the good things that C++ has, without the cruft: long compile times, edge cases, etc.
It's pretty amazing how the C++ ISO group managed to add things without breaking backward compatibility, so imagine if they agree to explicitly deprecate thing. I just hope that one day, there will be a split between "old C++", that will still be kept compatible to not break old codebases, and "new modern C++".
I'm not an expert, but I think it might be doable, maybe with some compile flags? I don't really think it would break the ABI or break binary compatibility between old and new C++. I don't know.
In theory I think you could teach a similar subset of C++ to what's in safe Rust. But you immediately run into a big problem that all the rest of C++ is still there and not knowing about it can hurt you, whereas safe Rust is promised by the unsafe Rust that you don't need to care why any of this is possible.
I don't understand the argument that Rust is hard to learn, unless you also mean C++ is hard to learn. If you come from C++, I don't believe the borrow checker should be that hard to learn; after all it's kind of just an extension of RAII semantics. It's far more straight concept than forward than C++ templates.
Oi vey. This sort of QA is exactly what's wrong with the "modern" C++.
It's just ugly.
I've been using C++ as my main language since it was barely out of C with Classes stage and it pains me to see how it evolved from a language that extended C in clever and welcomed ways into this piece of committee-designed monstrosity with not a trace of its former elegance left in it.
In C++ 03 they realise their pointer (inherited from C) is awful. Do they fix it? No, they add a new type named std::auto_ptr which is "smart". Thanks to operator overloading it can behave like the existing pointer. Kind of.
std::auto_ptr is pretty bad, and so in C++ 11 they deprecate auto_ptr and introduced std::shared_ptr and std::unique_ptr so now finally their language has halfway usable pointers, with these unwieldy names and some odd semantics you just have to get used to. C-style pointers remain all of: Simpler to use by virtue of their terse syntax, still necessary, and unsafe. std::auto_ptr still exists forever, but is officially deprecated.
At first C lacks strings, promoting string literals to a strange array with an extra zero value in it. Eventually C++ gets std::string and std::wstring but, nothing of value about these strings is defined, so they're both pretty useless and of course dangerous. In C++ 17 it gets std::string_view which finally expresses one of the things you probably first wanted, but it provides no enforcement of the most obvious expectation you have - C++ allows a string_view to outlive the underlying string, causing Undefined Behaviour. In C++ 20 you get to use ranged behaviour, and, that extra zero value bites you hard because of course you didn't mean the zero value but in C++ that's "part" of your string. They also, finally, decide that UTF-8 might be a good idea and define yet another type of string to mean UTF-8 encoded, about five years after everybody who matters was of course encoding strings as UTF-8.
> std::auto_ptr still exists forever, but is officially deprecated.
std::auto_ptr is fully removed since C++17.
I agree with most of your comment, but I think it reinforces the grandparent's point. The warts of std::string and null terminated strings are not "problems introduced by C++'s other recent additions".
Shared_ptr and unique_ptr are not replacement for raw C pointers. And the reason they are not blessed with language syntax is because user can and often do implement their own, so those two library types are not particularly special.
Agreed. And a huge strength of C++ is that you can write low-level classes like these and have them be fully performant and have reasonable native-feeling syntax. Almost no other languages provide that.
Non-owning pointers are a genuinely useful tool, but as a tool they're more welding torch than hammer, if that's what you needed it's really what you needed - but most often it probably isn't what you needed at all and will cause more harm than good.
So it's strange to provide them with stuff like terse built-in syntax, while not providing anything similar for the thing people will want all the time, ownership. Result: The Right Thing™ is harder to do and wastes visual space compared to the Wrong Thing. A "correct" program ends up full of boilerplate just to get what you'd obviously want.
> Eventually C++ gets std::string and std::wstring but, nothing of value about these strings is defined, so they're both pretty useless and of course dangerous. In C++ 17 it gets std::string_view which finally expresses one of the things you probably first wanted
Such as what? std::string_view represents exactly one thing, and that’s a slice into a string with a lifetime longer than it. That’s basically it. There aren’t really and other additional feature to it, so I feel like you may be confused as to what it does.
A lot of your criticism makes sense. I don't follow this one, though: `string_view` is *by definition` non-owning. Was non-owning the whole thing you wanted? If so, why are you surprised by lifetime issues? If non-owning isn't your main desire, what's wrong with `std::string`?
Probably in C++ context this feels unreasonable but I'm arguing that C++ is wrong here. It's OK for the language to enforce that the lifetime of the string underpinning a slice must equal or exceed the lifetime of the slice despite the slice not "owning" the string.
In 2011 there probably wasn't a mainstream non-GC language doing that. C++ would have been innovating here, and maybe C++ in 2011 didn't want to be innovating. By 2017 when C++ 17 introduces std::string_view it's behind the curve, this is not an exciting new feature, it's a weak imitation of what everybody else was already doing better.
Whoever reads this , note that its a very outdated rant (although some parts are still true).
> Well, the standard library doesn't have matrices or regular expressions, either
C++ does provide a standard regex library now, matrices can be made either with vectors or you can use boost libs for it.
> The standard library doesn't support listing directories
Same for filesystems, it now comes with the standard set of libraries
For C++, it’s time and time again seen, that Boost libs are used as the experimentation stage to filter things and add to the C++ standard library, and boost is pretty feature rich with all sorts of utilities you might need to build a robust program.
> In C++, things are different - there are no modules.
C++20 is adding features related to modules support like in other programming languages (haskell, etc).
Sure C++ is an old programming language with too many weird features, which end up hurting devs , but as long as you stick to the fine features.
C++ is pretty fun to use.
It has type inference now, those ugly looking long type-declarations, can easily be fixed by appropriate use of the “using” keyword in local scope (if you’re that worried about polluting the global namespace i.e)
I think C++ gets unnecessary hate , every language has its quirks, you just avoid the bad stuff and stick with the good bits.
> I think C++ gets unnecessary hate , every language has its quirks, you just avoid the bad stuff and stick with the good bits.
C++ is the single most complex programming language ever devised. I don't think it deserves hate per se, but it there is no other language as hard to learn, as hard to write safe code for, and as hard to read.
Even your example of type inference is actually very hard to learn. If I do `auto x = 7;` will x be const or not? If I have `const int &y = 7; auto x = y;` will x be const int? int? const int&? What if it's a pointer, or volatile instead?
The problems you mentioned about type inference are valid, but this issue lies is any programming language which provides this feature, and a lot of languages which are promoted and looked upto on HN (like Haskell) heavily use type inference.
The simplest answer to your question of how to know what type the compiler will choose for type inferenced variables, is by compiling an example code snippet and printing out the type using either something like decltype or typeid (preferably the former).
But the thing is, the point of type inference is you want to abstract away the type, if you want to explicitly be aware of the type at all times, you should explicitly state it in declarations.
The issues described is inherent in the very notion of letting the compiler infer the type for you, which leads to this ambiguity.
If you want to avoid it, then best to be explicit, if you dont care about the type and are using typeinference to build compile time simple template functions using lambda and the type inferred parameters.
Then type inference is pretty handy and leads to easy to read code.
It’s a tool, when you feel it’s getting in your way at writing code, you remove the tool/feature from your code.
> but this issue lies is any programming language which provides this feature, and a lot of languages which are promoted and looked upto on HN (like Haskell) heavily use type inference.
> The issues described is inherent in the very notion of letting the compiler infer the type for you, which leads to this ambiguity.
It's not the same thing. Types and type qualifiers are different things, and few languages have type qualifiers in the same way as C++. In fact, auto, unlike type inference in any other language, does NOT match the full type of the variable - that's why things like `volatile auto &x = y` exist.
> But the thing is, the point of type inference is you want to abstract away the type
That's the point in other languages. In C++ it's much more just shorthand for the horribly long type names you end up with in templates.
C++ had generic code for many years before auto (and decltype) existed. It definitely makes things easier, and I believe there are some types today that work with auto but can't be named otherwise. But, it's not directly required outside of these.
Template parameter inference, which mostly uses the same rules, is probably required in some SFINAE constructs, but is also not essential in other places.
I don't think anyone hates C++, they hate how it makes them feel when they have to work in it.
Also, I think it's a bit ludicrous to say "just use a subset" because there's no definitive guide to "sane" or safe subset.
Also, regarding boost: I'm extremely conflicted about it... On one hand, it's made my life easier, but on the other it feels like once you add it to the codebase it metastases everywhere; it's not a C++ codebase anymote, but a boost codebase. Also, it ill-composes with most things that aren't boost (and sometimes even within other parts of boost, because boost is an amalgamation of libraries)
This confuses me even more than C++ usually does. From the first answer:
> struct foo{ int const x; };
> void some_func(foo*);
> int bar() {
> foo f { 123 };
> some_func(&f);
> return f.x;
> }
> bar will always return 123. The compiler may generate code that actually accesses the object. But the object model does not require this. f.x is a const object (not a reference/pointer to const), and therefore it cannot be changed.
I like to think I know quite a bit about C++, but I don’t get this at all. Why is f.x a const object even when accessed through a non-const variable? f isn’t const; there’s no const_cast here. At first glance, this looks like a bug in the spec, roughly like confusing
int const *p
(pointer to const) with
int const *const p
(const pointer to const). Only the latter will always point to the same thing.
What am I missing?
(Edit: Since it’s a bit hidden in the SO question, the idea is that some_func does something like this:
void some_func(foo *f) {
new(f) foo { 42 };
}
Note that there’s no const_cast here, and f’s storage isn’t const, so this isn’t UB to the best of my knowledge.
BTW, in case it’s not obvious, it’s not all that helpful to reply to the question why the spec is a certain way by tautologically repeating the spec.)
(Edit 2: Unfortunately also very typical for C++ discussions, I just love how people think that C++ confusing me must mean that I’m dumb, and then condescendingly post a wrong answer. Rule of thumb: If you’re confident in your ability to understand C++, you don’t understand it very well.)
The constiness of a pointer can be casted away while the constiness of a value variable cannot — it’s UB.
foo::x is declared const, so it’s always const.
I realize that, but no variable is const, only the field is. And you don’t need a const_cast to change the value, as the example in the question shows, some_func can do:
I don’t think so. According to https://stackoverflow.com/a/39382728 (the original SO answer that prompted the linked SO question, and which quotes the relevant parts of the spec), the allocation is not UB, but accessing the member after that (at least without the std::launder trick) is UB. It’s precisely this latter part that I don’t think is very reasonable to have in the spec.
The issue is that code outside of some_func does not know that f has been destroyed and a new object created in the same memory location, thus wouldn't know that the const member foo::x now has a new value.
As for why the spec prohibits the modification of const values: one of the reasons for compilers being allowed to treat const values as being truly immutable is for performance. If the compiler can see the actual value then it can avoid the memory read entirely, and if it doesn't know the actual value then it only needs to do one memory read.
If those const values could change without the compiler being aware then it now needs to do an awful lot more fetching from memory as it cannot guarantee that the value did not change. Some of it could be optimized away, but it would still make code a lot slower if - for example: mixing reads from const ints with pointer/reference manipulation of ints - as due to potential aliasing the compiler would likely have to read those const ints from memory each time.
Note that accessing the new object via the pointer to placement new is perfectly fine, the issue is dereferencing the old pointer that logically point to an object whose lifetime has been terminated even if it mught actually contain the same bit pattern as the new pointer. I think this is related to the ill-defined concept of pointer provenance.
Launder let you discard any provenance info for the old pointer and treat it as a new one.
Tricky and very ill-defined, and according to the another comment elsethread this now doesn't require launder anymore (it probably was proven unworkable in practice).
> no assignment operator is generated for a struct with a const member.
I wanted to use const fields to enforce the logical invariant that a field never changes for the lifetime of an object, until the entire object is overwritten by a newly constructed object. Unfortunately C++ deleting the move-assignment operator means it's no longer practical to do so (though you can use placement-new for POD data where it's safe to not run the destructor, and even then it's obscure and will leak memory and possibly is UB as soon as you add an owning type). Instead, I keep a mutable field and have to remember to never mutate it when writing hundreds of lines of method implementations, and when I come back to the code months later and edit it, and someday I may slip up.
Regarding your EDIT: that is not a valid way of modifying x. When you try to assign a new foo that way, what you are implicitly doing is calling the copy assignment operator. This normally works by copying each field from the input object to the destination object, but, since the field x is const, it cannot be re-assigned to the input object's x. In fact, very often the compiler will generate a copy assignment operator implicitly for you, but not in this case, because it can't copy values into a constant field.
Also, since you have a local pointer f within some_func, so re-assigning a new object of type foo to it will not alter the original foo. This is not implied by your original comment but it's something one could wonder.
Note that per some comments and modifications to older questions, the standard has actually moved to the sane world and no longer allows this "optimization" [1][2]. So std::launder is no longer required for this purpose - just the other ones.
I will try to explain the logic as I understand it, and hopefully someone more knowledgeable will point out anyplace that I am wrong.
The problematic code was this:
struct X { const int n; };
union U { X x; float f; };
...
U u = {{ 1 }};
X *p = new (&u.x) X {2};
cout << u.x.n; //is this UB?
After the placement new, we are in the case from this piece of the standard ("the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied"). u.x is "the name of the original object" - can we use it to manipulate the new object that placement new put in there?
In the old standard, we can if "the type of the original object [...] does not contain any non-static data member whose type is const-qualified". Since X::n is const qualified, u.x can't be used to refer to the new object - enter std::launder.
However, the new standard modifies this exception: we can still use u.x if "the original object is neither a complete object that is const-qualified nor a subobject of such an object". Neither u nor u.x are const-qualified objects, nor are they a sub-object of a const-qualified object. So, u.x now refers to the new object created by placement new.
Still, std::launder seems to be needed for other cases, mostly related to reinterpret_cast. The C++ Reference site [0] shows these examples.
> (Edit 2: Unfortunately also very typical for C++ discussions, I just love how people think that C++ confusing me must mean that I’m dumb, and then condescendingly post a wrong answer. Rule of thumb: If you’re confident in your ability to understand C++, you don’t understand it very well.)
I sat next to a guy on the C++ Standards Committee and he was the first to admit that he didn't even fully understand C++. The abstract machine is monstrously complex.
So many people are weirded out in this thread, but unless you use placement-new there’s no need to know about this at all… This is just an escape hatch to solve a weird interaction between const members and placement-new.
Everything in C++ is just there to solve a weird interaction between <feature that’s in your codebase> and <feature that’s also in your codebase>. The “just learn a subset” crowd is wrong, you really do need to know everything.
Gotta love these C++ FUD arguments! Can you tell us an example where a library’s internal usage of placement new results in bugs unless the consumers use std::launder?
Isn't this exactly what this argument shows? In older versions of C++, if you have an object with a non-const sub-object with a const member, and you give this object to a library, and then that library uses placement new to replace the sub-object within your object, you had to use std::launder to keep referring to the fields of your object, or you'd invoke UB.
In newer C++, thankfully, this is no longer required.
Edit: found a committee paper discussing exactly such cases:
Obj x;
x.use(); // ok
mutate(x); // does this call placement new?
x.use(); // may have undefined behavior
std::launder(&x)->use(); // may be necessary
x.use(); // may still have undefined behavior
If you don't understand what placement new is, you don't know whether libraries using placement new affects you.
I did not write this comment about placement new specifically. It's just a thing that annoys me about the "learn a subset" idea. You still need to know at least enough about every feature to know whether it affects you.
Placement new is perhaps not the best example there because it is relatively niche and self contained, true.
If you're going to try to be clever/snide, at least come up with a better analogy...
Using Linux effectively doesn't mean you need to learn its source code. You need to know about its features (and trade-off, etc).
You don't need to know a language implementation's (e.g. gcc/clang for C++) source code to use a language well either, do you? Obviously you don't; it's a fools errand if all you ever want is to use it effectively.
You need to know about features (and trade-off, etc), how they were designed in isolation, and more importantly how they were designed to fit/connect together.
115 comments
[ 4.7 ms ] story [ 173 ms ] threadIf you really want to abstract this sort of thing, a good abstraction would be a write-only type, "space", representing memory with no content. Then make it possible to destroy an object and convert it to empty "space", or construct an object on empty "space". This creates a clear distinction between reusing memory and abusing the type system.
I think that the whole object model need to be formalized from scratch (similar to what was done for the concurrent MM) but that's still hard to do while preserving all the properties that current programs expect.
But in fact memcpy to a local is very often a better alternative, pre-C++20. Compilers optimize it away.
reinterpret_cast guarantees that it is in-place, so it's the right tool for 0-copy.
What when your compiler starts to take advantage of latitude extended by the Standard, leading your program to produce sporadic, non-reproducible wrong results?
It is best not to code any UB. But it is often hard to be certain, around reinterpret_cast.
You rarely want to be spending time fiddling with the optimizer. Your code shouldn't rely on the optimizer to be efficient: don't use constructs whose semantics mean you are making a copy in places where you don't want a copy.
reintrepret_cast is hard to use correctly, but you only have to get it right once, and it has the right semantics, so no compiler changes will suddenly make it wrong or slow. memcpy and bit_cast have the wrong semantics, so they may change for unexpected reasons. In fact, it would be perfectly reasonable for a future version of the compiler to never optimize away the copy implied by memcpy and bit_cast.
So yes, you can confidently rely on gcc or clang to merge local variables, even at -O1. If they suddenly don't do that after an update, it's a serious regression, and their mailing lists should be more than happy to receive that bug report.
Not to mention, in an example like processing packets, the two views of the data WILL be simultaneously live: you're taking raw bytes on one side, interpreting some part of them as a struct to decide what's happening next, then sending the raw bytes somewhere else.
EDIT: Basically the code would look something like this:
Will this copy the header or not? How about this? Also note that in C we have a somewhat cleaner solution: This would of course be UB in C++ (reading from a union member that is not the last one written to).(agree about the two views being live at the same time and memcpy being suboptimal here).
For small objects bitcast works just fine because the result end up in registers were you would have loaded the original objects anyway.
Unfortunately there are also still a lot of platforms that have yet to get C++20 support and memcpy is not a viable alternative. Hopefully this is less of an issue in the future as major MCU orgs like TI have started switching to LLVM/Clang based compilers which can follow upstream with little to no changes needed.
Edit: I just did some digging and bit_cast does not work for the aforementioned use case since it can copy the data to a new location in memory. This prevents us from being able to reliably use it to write to memory registers.
The more clarity is available as to what you meant, the more aggressive the optimiser can be in delivering precisely that. If you don't express exactly what is intended, the optimisers end up getting muzzled because the effect is so confusing.
Maybe reinterpret_cast on StandardLayout with the correct alignment really does express exactly what we want here, I'm not sure. If so you're correct there wouldn't be a difference because a valid optimisation will always be exactly what everybody wanted. But if there are any other uses of these techniques the optimiser has to be pessimistic. That's why I prefer to be as explicit as possible.
This is how so many extra types of cast came into existence in C++ in the first place right, the recognition that casting could have different intents.
Surely if you are unsure the default position should be to go to the obvious and simple solution instead of overengineering it? But I might be completely misunderstanding your point.
[If the struct is not correctly aligned, I would either memcpy or apply any required compiler pragmas, while cursing the protocol designer]
In the future, rust might gain "Safe Transmutes", which would allow you to safely transmute from one type to another under some limited conditions. See https://github.com/rust-lang/lang-team/issues/21
If you want to be fancy, there's https://lib.rs/rkyv deserialization framework that will auto-generate appropriate zero-copy code for you.
There's also serde (de)serialization framework and bincode format, which aren't exactly zero-copy, but are fast 1-copy.
For types with such attributes, coercions are safe. C++ could potentially have something like "reinterpret_cast" which worked only on such types. This would make the risky form of reinterpret_cast less necessary, and some projects might disallow it.
It's always hard to retrofit safety to a code base. You get a lot of pushback.
I've been left a strong impression that either I do it "safely" or I want to do it totally unsafe, there's often like no middle ground. No I don't want to suddenly accidentally double-dereference a pointer or cast away const, I just want to use this memory mapped peripheral. That is very unfortunate and short-sighted, IMO, considering that layer is where all safety and security begins from.
A key concept in Rust is that no code can have access to two mutable references to the same object at the same time. That's the issue with std::launder, of course. The Rust mechanism for doing it enforces stronger restrictions than absolutely necessary. Not that enforcing the minimal set of restrictions is easy.
The general argument of Rust enthusiasts is that you don't need that. But many of them are writing web back-end stuff, which does not have much state. I'm writing a client for a virtual world, which has an entire visible scene of constantly changing state. Representations for that sort of thing have extensive cross-links. Hammering that into the Rust model tends to result in things such as having to look up things in hashes where you'd use a direct link in C++.
What's desirable is safe single ownership with backlinks. That's hard. You need something like a weak reference to a singly owned object, along with the compile time checking needed to prevent its misuse. Right now, you can do that with reference counted weak links, which are checked at run time. But you're often calling ".borrow()" to follow a link, and that will fail if the mutable borrow count becomes > 1. If that check could be replaced with static analysis by the borrow checker, more safe data structures would be possible. Trees with backlinks, doubly linked lists, etc.
Not having this means you can reach the point where your program is one error away from compiling, and days of rewrite are needed to fix the problem. This is where unsafe code tends to sneak in, as an escape hatch.
Would Rust allow to have all data in 'structs' (no private data), and make all logic pure functional (functions without side effects, data in, data out)? That way, you won't need circular references or stateful objects and it simplifies memory management.
Admittedly this is a somewhat unusual application. It's somewhere between writing an MMO client and a web browser. I once wrote a RSS feed reader in Rust, and that didn't have any of these hard problems.
I don't mean to make a new language like Zig or rust or nim. I mean just a "saner" C++ without all the backward compatibility that is holding back the language.
For contrast, the Ada language (which is larger than C but smaller than C++) takes this idea quite seriously. There are various 'well known' subsets of Ada with interesting properties and tooling. These subsets are called profiles. There's SPARK, a very minimalist subset tailored for formal verification, and Ravenscar, for concurrent hard real-time systems. Profile compliance is checked by the compiler itself. (If I understand correctly, the profiles are defined using Ada's rough equivalent of #pragma in C and C++.)
In the C and C++ worlds there are subsets like Google's C++ style guide, LLVM's C++ style guide, MISRA C, and MISRA C++, but their tools (i.e. static analysis tools to check for compliance) are generally separate from the compiler, for what that's worth.
You still get all the compile-time constructions, RAII, memory protection, lambdas, modules and so on.
[0] https://dlang.org/spec/betterc.html
I think they were inspired by C++, but decided one "plus" to the language was enough. Maybe the "pluses" add exponential complexity, and C++ hasn't reached the end of its second plus stage yet. /jk
(note: I am not a C++ dev, so this is just my assumption/derivation)
Also, it would not be difficult to link against existing C++ libraries, or at least to wrap them using extern C.
The goal would be a language that has the good things that C++ has, without the cruft: long compile times, edge cases, etc.
It's pretty amazing how the C++ ISO group managed to add things without breaking backward compatibility, so imagine if they agree to explicitly deprecate thing. I just hope that one day, there will be a split between "old C++", that will still be kept compatible to not break old codebases, and "new modern C++".
I'm not an expert, but I think it might be doable, maybe with some compile flags? I don't really think it would break the ABI or break binary compatibility between old and new C++. I don't know.
Most of C++'s problems come from it trying to offer 0-cost abstractions, at any cost to language complexity.
It's just ugly.
I've been using C++ as my main language since it was barely out of C with Classes stage and it pains me to see how it evolved from a language that extended C in clever and welcomed ways into this piece of committee-designed monstrosity with not a trace of its former elegance left in it.
std::auto_ptr is pretty bad, and so in C++ 11 they deprecate auto_ptr and introduced std::shared_ptr and std::unique_ptr so now finally their language has halfway usable pointers, with these unwieldy names and some odd semantics you just have to get used to. C-style pointers remain all of: Simpler to use by virtue of their terse syntax, still necessary, and unsafe. std::auto_ptr still exists forever, but is officially deprecated.
At first C lacks strings, promoting string literals to a strange array with an extra zero value in it. Eventually C++ gets std::string and std::wstring but, nothing of value about these strings is defined, so they're both pretty useless and of course dangerous. In C++ 17 it gets std::string_view which finally expresses one of the things you probably first wanted, but it provides no enforcement of the most obvious expectation you have - C++ allows a string_view to outlive the underlying string, causing Undefined Behaviour. In C++ 20 you get to use ranged behaviour, and, that extra zero value bites you hard because of course you didn't mean the zero value but in C++ that's "part" of your string. They also, finally, decide that UTF-8 might be a good idea and define yet another type of string to mean UTF-8 encoded, about five years after everybody who matters was of course encoding strings as UTF-8.
std::auto_ptr is fully removed since C++17.
I agree with most of your comment, but I think it reinforces the grandparent's point. The warts of std::string and null terminated strings are not "problems introduced by C++'s other recent additions".
So it's strange to provide them with stuff like terse built-in syntax, while not providing anything similar for the thing people will want all the time, ownership. Result: The Right Thing™ is harder to do and wastes visual space compared to the Wrong Thing. A "correct" program ends up full of boilerplate just to get what you'd obviously want.
Such as what? std::string_view represents exactly one thing, and that’s a slice into a string with a lifetime longer than it. That’s basically it. There aren’t really and other additional feature to it, so I feel like you may be confused as to what it does.
In 2011 there probably wasn't a mainstream non-GC language doing that. C++ would have been innovating here, and maybe C++ in 2011 didn't want to be innovating. By 2017 when C++ 17 introduces std::string_view it's behind the curve, this is not an exciting new feature, it's a weak imitation of what everybody else was already doing better.
> Well, the standard library doesn't have matrices or regular expressions, either
C++ does provide a standard regex library now, matrices can be made either with vectors or you can use boost libs for it.
> The standard library doesn't support listing directories
Same for filesystems, it now comes with the standard set of libraries
For C++, it’s time and time again seen, that Boost libs are used as the experimentation stage to filter things and add to the C++ standard library, and boost is pretty feature rich with all sorts of utilities you might need to build a robust program.
> In C++, things are different - there are no modules.
C++20 is adding features related to modules support like in other programming languages (haskell, etc).
Sure C++ is an old programming language with too many weird features, which end up hurting devs , but as long as you stick to the fine features.
C++ is pretty fun to use. It has type inference now, those ugly looking long type-declarations, can easily be fixed by appropriate use of the “using” keyword in local scope (if you’re that worried about polluting the global namespace i.e)
I think C++ gets unnecessary hate , every language has its quirks, you just avoid the bad stuff and stick with the good bits.
C++ is the single most complex programming language ever devised. I don't think it deserves hate per se, but it there is no other language as hard to learn, as hard to write safe code for, and as hard to read.
Even your example of type inference is actually very hard to learn. If I do `auto x = 7;` will x be const or not? If I have `const int &y = 7; auto x = y;` will x be const int? int? const int&? What if it's a pointer, or volatile instead?
The simplest answer to your question of how to know what type the compiler will choose for type inferenced variables, is by compiling an example code snippet and printing out the type using either something like decltype or typeid (preferably the former).
But the thing is, the point of type inference is you want to abstract away the type, if you want to explicitly be aware of the type at all times, you should explicitly state it in declarations.
The issues described is inherent in the very notion of letting the compiler infer the type for you, which leads to this ambiguity.
If you want to avoid it, then best to be explicit, if you dont care about the type and are using typeinference to build compile time simple template functions using lambda and the type inferred parameters. Then type inference is pretty handy and leads to easy to read code.
It’s a tool, when you feel it’s getting in your way at writing code, you remove the tool/feature from your code.
> The issues described is inherent in the very notion of letting the compiler infer the type for you, which leads to this ambiguity.
It's not the same thing. Types and type qualifiers are different things, and few languages have type qualifiers in the same way as C++. In fact, auto, unlike type inference in any other language, does NOT match the full type of the variable - that's why things like `volatile auto &x = y` exist.
> But the thing is, the point of type inference is you want to abstract away the type
That's the point in other languages. In C++ it's much more just shorthand for the horribly long type names you end up with in templates.
Template parameter inference, which mostly uses the same rules, is probably required in some SFINAE constructs, but is also not essential in other places.
Also, I think it's a bit ludicrous to say "just use a subset" because there's no definitive guide to "sane" or safe subset.
Also, regarding boost: I'm extremely conflicted about it... On one hand, it's made my life easier, but on the other it feels like once you add it to the codebase it metastases everywhere; it's not a C++ codebase anymote, but a boost codebase. Also, it ill-composes with most things that aren't boost (and sometimes even within other parts of boost, because boost is an amalgamation of libraries)
I like to think I know quite a bit about C++, but I don’t get this at all. Why is f.x a const object even when accessed through a non-const variable? f isn’t const; there’s no const_cast here. At first glance, this looks like a bug in the spec, roughly like confusing
(pointer to const) with (const pointer to const). Only the latter will always point to the same thing.What am I missing?
(Edit: Since it’s a bit hidden in the SO question, the idea is that some_func does something like this:
Note that there’s no const_cast here, and f’s storage isn’t const, so this isn’t UB to the best of my knowledge.BTW, in case it’s not obvious, it’s not all that helpful to reply to the question why the spec is a certain way by tautologically repeating the spec.)
(Edit 2: Unfortunately also very typical for C++ discussions, I just love how people think that C++ confusing me must mean that I’m dumb, and then condescendingly post a wrong answer. Rule of thumb: If you’re confident in your ability to understand C++, you don’t understand it very well.)
As for why the spec prohibits the modification of const values: one of the reasons for compilers being allowed to treat const values as being truly immutable is for performance. If the compiler can see the actual value then it can avoid the memory read entirely, and if it doesn't know the actual value then it only needs to do one memory read.
If those const values could change without the compiler being aware then it now needs to do an awful lot more fetching from memory as it cannot guarantee that the value did not change. Some of it could be optimized away, but it would still make code a lot slower if - for example: mixing reads from const ints with pointer/reference manipulation of ints - as due to potential aliasing the compiler would likely have to read those const ints from memory each time.
Launder let you discard any provenance info for the old pointer and treat it as a new one.
Tricky and very ill-defined, and according to the another comment elsethread this now doesn't require launder anymore (it probably was proven unworkable in practice).
> if you allocate a new object in the storage of the old one, you cannot access the new object through pointers to the old
Which implies the UB happens in the caller.
The access in the caller -- `return f.x` -- is not through a pointer.
Because the member is const.
It's a bit strange example because there's no reason to have mutable pointer to a struct with only const members. A better example would maybe be:
EDIT: On second thought I'm also a bit confused. Wouldn't this be a "safe" way of modifying `f.x`?I wanted to use const fields to enforce the logical invariant that a field never changes for the lifetime of an object, until the entire object is overwritten by a newly constructed object. Unfortunately C++ deleting the move-assignment operator means it's no longer practical to do so (though you can use placement-new for POD data where it's safe to not run the destructor, and even then it's obscure and will leak memory and possibly is UB as soon as you add an owning type). Instead, I keep a mutable field and have to remember to never mutate it when writing hundreds of lines of method implementations, and when I come back to the code months later and edit it, and someday I may slip up.
Rust has no const fields at all.
Also, since you have a local pointer f within some_func, so re-assigning a new object of type foo to it will not alter the original foo. This is not implied by your original comment but it's something one could wonder.
[0] https://stackoverflow.com/a/70419156 (first part)
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p197...
[2] https://wg21.link/p1971r0
So is launder needed for anything now?
The problematic code was this:
After the placement new, we are in the case from this piece of the standard ("the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied"). u.x is "the name of the original object" - can we use it to manipulate the new object that placement new put in there?In the old standard, we can if "the type of the original object [...] does not contain any non-static data member whose type is const-qualified". Since X::n is const qualified, u.x can't be used to refer to the new object - enter std::launder.
However, the new standard modifies this exception: we can still use u.x if "the original object is neither a complete object that is const-qualified nor a subobject of such an object". Neither u nor u.x are const-qualified objects, nor are they a sub-object of a const-qualified object. So, u.x now refers to the new object created by placement new.
Still, std::launder seems to be needed for other cases, mostly related to reinterpret_cast. The C++ Reference site [0] shows these examples.
[0] https://en.cppreference.com/w/cpp/utility/launder#Example
I sat next to a guy on the C++ Standards Committee and he was the first to admit that he didn't even fully understand C++. The abstract machine is monstrously complex.
IIRC the issue was that vector construct one object at a time but then access it as if it was part of an array which was never constructed
In newer C++, thankfully, this is no longer required.
Edit: found a committee paper discussing exactly such cases:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p053...
I did not write this comment about placement new specifically. It's just a thing that annoys me about the "learn a subset" idea. You still need to know at least enough about every feature to know whether it affects you. Placement new is perhaps not the best example there because it is relatively niche and self contained, true.
Using Linux effectively doesn't mean you need to learn its source code. You need to know about its features (and trade-off, etc).
You don't need to know a language implementation's (e.g. gcc/clang for C++) source code to use a language well either, do you? Obviously you don't; it's a fools errand if all you ever want is to use it effectively.
You need to know about features (and trade-off, etc), how they were designed in isolation, and more importantly how they were designed to fit/connect together.
a function is much better, it's explicit