Why introduce `std::launder` rather than have the compiler take care of it?
stackoverflow.com
stackoverflow.com
> 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.)
void some_func(foo *p) {
new(p) foo { 42 };
}
That’s not UB.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:
struct foo{ int const x; int y; };
void some_func(foo *f) {
foo.y = 0;
}
int bar() {
foo f { 123, 456 };
some_func(&f);
return f.x; // must always return 123
}
EDIT: On second thought I'm also a bit confused. Wouldn't this be a "safe" way of modifying `f.x`? void some_func(foo *f) {
*foo = foo { 5, 5 }; // probably wrong syntax? I don't write so much C/C++
} void some_func(foo *f) {
new(f) foo { 5, 5 };
}
because 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.
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...
So is launder needed for anything now?
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.
[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.
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.
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:
char* buf = new char[100];
read(buf, sizeof(header));
header h;
memcpy(&h, buf, sizeof(header))
get_destination(h).write(buf);
Will this copy the header or not? How about 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: union buf_header {
header h;
char buf[100];
} buffer;
read(buffer.buf, sizeof(header));
get_destination(buffer.h).write(buffer.buf);
This would of course be UB in C++ (reading from a union member that is not the last one written to). header h;
read((char*)&h, sizeof(h));
get_destination(h).write((const char*)&h, sizeof(h));
Is well defined even under strict aliasing.(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.
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...
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 behaviorI 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.
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.
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)
a function is much better, it's explicit