Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
std::launder: the most obscure new feature of C++17 (2016) (miyuki.github.io)
41 points by goranmoomin on April 4, 2024 | hide | past | favorite | 52 comments


A post about an obscure C++ feature on Hacker News? Comments are going to be a dumpster-fire as usual.

https://en.cppreference.com/w/cpp/utility/launder

"template <class T> [[nodiscard]] constexpr T* launder (T* p) noexcept;

Provenance fence with respect to p. Returns a pointer to the same memory that p points to, but where the referent object is assumed to have a distinct lifetime and dynamic type."

It's a function for the compiler for low-level memory management and lifetime management code (i.e. code almost no C++ "end-user" writes) to turn off compile-time tracking and optimisations that might not be correct. Typically only used if you're starting the lifetime of one object over or inside another. Essentially, when you want the compiler to be dumb you launder the pointer to make the compiler intentionally forget the complex compile-time state tracking that compilers do and make the compiler pretend that pointer is actually a brand new object it knew nothing about.

Someone brought up the volatile keyword: volatile is for turning off compiler assumptions about what a value might be at run-time. For example, if you read from a regular int twice in a row with no write in between, then the compiler would likely remove the second read and reuse the first value (better code-gen) as it knows the value could not have changed. The volatile keyword is how you tell the compiler that it cannot reliably observe all changes to the variable so every read must be performed (same for writes).

launder and volatile are similar in so much that they exist to tell the compiler not to make assumptions about the values/objects, but that's about it. They are not interchangeable.

But let's all pretend this function is something everyone using C++ needs to use to farm some internet points.


> that prevents the optimizer from performing constant propagation

I understand how it works, and why you would even maybe want this, but this just straight-up seems like a mistake. It breaks const-invariance, which I guess is no longer invariant. Is just any proposal making into C++ these days?

> He tells us that this function might be handy when dealing with containers of const-qualified elements.

Then just.. don't.. const-qualify your elements? What a neat footgun, not even consts are safe in C++ now.


C++ is a weird language that essentially combines both a high-level type system whose details of representations and whatnot can be freely modified by a sufficiently smart compiler with a low-level bytemucking type system, and the interface between the two systems is gnarly and confusing and std::launder is lying in a pit on this border.

The point of std::launder is to let you use an object that would otherwise be undefined behavior to use. The original motivating example for std::launder essentially boils down to "you can't properly make std::vector<T> if T has a const member variable without std::launder," although C++ later did change the object model (after this feature had been accepted!) so that std::vector works without std::launder.

The residual use of std::launder has to do with vtables (which are important properties of an object yet not properly part of the object model because it's a high-level thing, not a low-level thing) after placement new, which is an extremely niche thing that truly almost nobody needs to care about and probably doesn't deserve a place in the standard library. libc++ overloads it to use it as a way to indicate intentional strict aliasing violations, but this is still UB per the standard.


I'm wondering if any uses at all would be left if programs were using strict provenance, a la:

https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-...

If I have a pointer to an object that no longer exists because I've futzed with a union or placement new or otherwise put a different object at that address, but I know that my pointer has the correct bit pattern (i.e. address on most systems) for the new object, then a strict provenance model says that I have no business dereferencing the pointer. But an API like with_addr() gives me a very explicit way to tell the compiler (and the CPU or runtime if provenance is being enforced!) what I'm doing, and the cases that could plausibly generate valid code will be well defined and work correctly.


The issue that's trying to be solved with std::launder is essentially around lifetime issues, which strict provenance doesn't really solve. Strict pointer provenance is about fixing the semantics of int-to-pointer, which requires tweaks to pointer-to-int. std::launder, by contrast, is trying to fix cases where memory is being validly reused after the language thinks the object is dead--it's the kind of thing you might need to use to implement an allocator, for example, although you already have screwy effective type rules breaking you that std::launder doesn't fix.


My point, which may or may not be entirely correct, is that the examples I've seen are, in effect, uses after a lifetime is over. They seem to boil down to something like:

    struct A {
        const int x;
    };
    
    A *ptr = <address of object 1>
    
    ... destroy object 1 and create object 2 in its place, so <old address of object 1> == <new address of object 2>
    
    do_something_with(ptr->x);
The language thinks ptr points to object 1, but it actually points to object 2. In a weak provenance model, maybe this is valid, and it gets the correct results if std::launder is used.

But it seems to me that, in a strict provenance model, accessing ptr->x is incorrect -- ptr has provenance for object 1, and object 1 is gone. The operation needed to restore access isn't something very generic like std::launder -- it's specifically the creation of a new pointer with provenance matching object 2. So you don't want:

    do_something_with(std::launder(ptr->x));
You want (very pseudo-codeish):

    do_something_with(with_addr(&object2, ptr)->x);
And I would hope that a good provenance implementation would not require an abomination like:

    do_something_with(launder(with_addr(&object2, ptr))->x);


They never were safe in C++. And no serious developer was expecting them to be perfectly safe, they are a tool for normal well-formed software to be a little less errer-prone. Everybody taking a class in C++ learns about const_cast and anyone clever eventually figures out how to use reinterpreter_cast for the similar things.

Launder is a replacement for calling compiler intrinsics during certain special operations, not everyday use. For one example: If I wrote a simple benchmarking library and shipped its source code then an optimizer could easily mingle my benchmarking library into another developer's benchmarked code. Launder provides at least some rudimentary options for minimizing this so artifacts of the implementation of my benchmark impact the code being measured less and in more predictable ways.

Other places this might be useful that I can think of off the top of my head might include: code that needs predictable binaries to avoid spectre and similar issues, working with precompiled binaries with headers but without source, code that is trying to leverage specific features of the underlying hardware possibly for maximum performance. I am sure there are more but disabling optimizations is super useful sometimes.


I think you're thinking of barriers?


C++’s const was never especially safe — contemplate what mutable does. And that one can cast pointers-to-nonconst to pointer-to-const.


It's always been a "programmer aid annotation" rather than anything that really affects code generation.

The only possible difference (from the resulting binary POV) I can think of is "static const" variables might be allocated in a read-only executable section?


It doesn't even have to be static. Const is powerful when used on non-pointer types. You can write something like this and the compiler will (on appropriate optimization levels) hard-code return value to 7777 regardless of what "mutate" does:

    extern void mutate(int *p);
    int func() {
        const int foo = 7777;
        mutate(&foo);
        return foo;
    }


Probably not, given the possibility that they can be const_casted back into mutable values. Locating static const values in read-only pages would result in crashes from valid statements in the language. In other words if you put a static const in a read only page, it would probably be considered a bug in your linker.


That doesn't rhyme with my understanding of C++ (which is limited, of course). This sounds like undefined.


Aren't they put into the .rodata section of the binary?

Crashing is exactly what happens, thankfully

https://godbolt.org/z/vKj9dcPMc


Generally yes, if you have a bunch of const data in your C++ code, it will go in .rodata. Depending on the compiler/flags/optimizations, of course. https://stackoverflow.com/a/44938843


const_cast is only for removing const from references or pointers that actually refer to a non-const object. You can't declare a const variable and then modify it with const_cast. See "Notes" at: https://en.cppreference.com/w/cpp/language/const_cast

The compiler can indeed put const objects in the executable's .rodata section, which means trying to modify it at runtime will probably cause a segfault (but it's all Undefined Behavior so who knows).


It is undefined behaviour to modify objects declared as const (except if those objects have mutable members, in which case the mutable members can be modified).

const int a = 1;

If you try to const_cast away the const of x to change its value, you have undefined behaviour. As for pointers, if you make a const pointer to a non-const object then all it means is that you cannot modify the object via that pointer, not that the object will never be modified.

int a = 1; int b = &a; const int c = &a;

a = 2;

Both b and c are now 2 and this is absolutely fine, and the compiler won't try to optimise out any reads of *c and assume its value is still 1 as const/non-const pointer aliasing is allowed (C has the restrict keyword, it's not in C++).


I don't understand why plain old volatile wouldn't be enough to prevent constant propagation.


I would expect volatile and const to be mutually exclusive.


For C++, you would be wrong. You can use it for variables that your code won’t change (hence const), but that can change due to outside events and hence can’t be cached in a register (hence volatile)

See https://www.embedded.com/combining-cs-volatile-and-const-key... for examples, https://en.cppreference.com/w/cpp/language/cv for a more formal description.


In a Memory Mapped device, a read can trigger side effects (in fact, I worked a lot with that), so is not only is about memory.


Yes, I'm aware, and thus I said "would". But I wouldn't make use of this without first reading the spec on that very carefully. Thankfully I've not needed this.


As far as complicated features that very few people understand or use and that are hard to implement correctly in compilers, std::launder ranks pretty high.

Topping the list, for me, would be memory_order_consume. It's so complicated and for such a slight extremely technical memory ordering optimization, compilers have mostly given up hope of ever implementing it.

There was even talk of outright removing it from C++, but looks like it still stands today. I've never seen it used correctly (and even if it were, compilers just throw up their hands at it)


I recall plenty of implementations turning into the same as a memory_order_acquire, which seems fine these days


> On all mainstream CPUs other than DEC Alpha, dependency ordering is automatic, no additional CPU instructions are issued for this synchronization mode, only certain compiler optimizations are affected (e.g. the compiler is prohibited from performing speculative loads on the objects that are involved in the dependency chain).

> Note that currently (2/2015) no known production compilers track dependency chains: consume operations are lifted to acquire operations.

https://en.cppreference.com/w/cpp/atomic/memory_order#Releas...


One of the main legitimate use cases for this feature is managing how the compiler interprets the lifetimes of runtime objects that are explicitly paged to disk. Many code bases rely on undefined behavior where the compiler coincidentally produces the expected behavior, which usually works, but I have seen occasional bugs in the wild from when it doesn't. You can use std::launder to guarantee that the compiler correctly interprets the lifetime of objects with these properties.


> explicitly paged to disk

Example would help. Most paging nowadays is implicit, transparent to the program.

Are you talking about some old-school overlays or something? Multiple objects in mass storage are mapped to the same address in memory, multiplexed in the time domain?


Paging is not transparent in high-scale and high-performance databases when using direct I/O to disk. It is not possible to make it transparent on many larger servers even if you wanted to because the silicon does not support a large enough virtual address space. This is a pretty common design problem in databases, or anything with huge amounts of data and storage really.

There were magic incantations that compilers informally respected to induce the correct behavior but it wasn’t always reliable and technically not a bug when it wasn’t. This provides an official and simple way to achieve the same effect without obscure magic.


Are high performance databases embedding non-POD C++ objects in explicitly paged memory?


Most of your runtime objects are expected to be pageable in databases designed to support very high storage density because it is necessary as a matter of robustness and persistability. Even single runtime metadata structures may not reasonably fit in RAM in some cases. There are constraints on these objects (e.g. no vtables) but they are typically not PODs.

Even if they were PODs, changing the types and instances of types at a specific memory address without destroying the objects residing there can have side-effects. The compiler does not expect a live memory address to be overwritten with an arbitrary bucket of bits by a DMA operation. DMA doesn't respect lifetime and ownership models as understood by a compiler, so it is a bit of a Deus Ex Machina as far as the compiler is concerned and is assumed to never happen.


> Example would help.

Almost any database.


'paged to disk'

I assume you're not talking about virtual memory here, but serialisation to disk or the network


Direct I/O from and to user space, no serialization. There are significant size and performance limits when using virtual memory to give objects a consistent address, hence why it is sometimes avoided.


std::launder has now become 'famously obscure'. I liked it before it was cool but nowadays indie kids like me prefer std::hardware_destructive_interference_size (https://en.cppreference.com/w/cpp/thread/hardware_destructiv...)


“Jim, management is complaining that we can’t hire anyone for the open C++ developer positions because the interviews are all about std::launder. What’s up?”

“Don’t you want to hire a top expert? It says right here that only five people in the world understand this feature. It’s the perfect filter to ensure that we don’t get random nobodies working on our legacy Win32 MFC application that manages hotel breakfast reservations. Trust the signal.”


There are so many things that engineers want to simplify their lives that don’t appear to be prioritised for the standard library, so it is rather surprising to see something like this, which can only complexify.


This isn't my example, but consider this situation: you are maintaining an old code base that has stuff from 30 to 40 years ago in it because it's still working and still making the company money. But it is old, came from before we understood dependency management, and came from before we had tests as a standard practice. Parts of it have to be built on some old compiler because no one has taken the effort to rewrite it and get it to work on the new compiler. This hasn't been done because millions of developer hours have been put into it and it could take hundreds of thousands of developer hours to redo it.

An obvious first step is to start by writing tests, and when you do so you can build little pieces of the old code on the new compiler but sometimes it produces wrong answers. If the old compiler worked a certain way and the new compiler works a different way it is often because the new compiler has a better optimizer and leverages better understandings of the assumption that most developers make. But some "clever" developer from back in the day did some shenanigans and leveraged undefined behavior that happened to do what they wanted. The maintenance options are either to leave it on the old compiler, rewrite the whole thing and hope you make it work right, or use std::launder to make the undefined behavior go away and hopefully make a minimal changes to retain the old behavior.

In this case launder is a middle path that hopefully lets maintainers keep most of the old code with the new platform, but hopefully without having to rewrite a huge amount of it.


Wait this is crazy. What's the legitimate use-case? Man I would hate to work on codebases that have these invariants violated.


The original use case was that std::vector's API was specifically designed to make it possible to have a vector of a type with const members, but it turned out to be impossible to implement without hitting UB. std::launder was introduced as a way to make the obvious implementation correct. Since then the object lifetime rules were adjusted to make it unnecessary for that.


Oh interesting. Thank you.


This is an April folks joke


> Don’t ask. If you’re not one of the 5 or so people in the world who already know what this is, you don’t want or need to know.

The attitude rings true though. I am convinced that C++ is proof that we have discovered alien technology, and it is awful.


The thing is that there is probably a lot of existing C++ code that is UB without std::launder (similarly to aliasing rules.) The main problem is that the C++ object lifetime rules are not well understood by most people writing C++ code.


I hadn't even considered the task of going through old code and using this to make it more compliant, this is a great use case for this feature.


It is not. It links to a lengthy StackOverflow thread about std::launder.

https://stackoverflow.com/questions/39382501/


The C++ language standard is not a funny joke.


Agreed. Brevity is the soul of wit, and all that.


It's the art of making the juggling with swords seem safe and easy while making a daily commute through a minefield of UB seem routine.


And there's a second minefield called the portable build solution, but we don't talk about it much.




I don't think this is true




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: