std::launder and strict aliasing rule
Asked Answered
L

2

12

Consider this code:

void f(char * ptr)
{
    auto int_ptr = reinterpret_cast<int*>(ptr); // <---- line of interest 
    // use int_ptr ... 
}

void example_1()
{
    int i = 10;    
    f(reinterpret_cast<char*>(&i));
}

void example_2()
{
    alignas(alignof(int)) char storage[sizeof(int)];
    new (&storage) int;
    f(storage);
}

Line of interest with call from example_1:

Q1: On the callside the char pointer is aliasing our integer pointer. This is valid. But is it also valid to just cast it back to an int? We know an int is within its lifetime there, but consider the function is defined in another translation unit (with no linktime optimization enabled) and the context is not known. Then all the compiler sees is: an int pointer wants to alias a char pointer, and this is violating the strict aliasing rules. So is it allowed?

Q2: Considering it's not allowed. We got std::launder in C++17. It's kind of a pointer optimization barrier mostly used to access an object which got placement new'ed into the storage of an object of other type or when const members are involved. Can we use it to give the compiler a hint and prevent undefined behavior?

line of interest with call from example_2:

Q3: Here std::launder should be required, since this is the std::launder use case described in Q2, right?

auto int_ptr = std::launder(reinterpret_cast<int*>(ptr));

But consider again f is defined in another translation unit. How can the compiler know about our placement new, which happens on the callside? How can the compiler (only seeing function f) distinguish between example_1 and example_2? Or is all above just hypothetical, since the strict aliasing rule would just rule out everything (remember, char* to int* not allowed) and the compiler can do what it wants?

Follow-up question:

Q4: If all code above is wrong due to aliasing rules, consider changing the function f to take a void pointer:

void f(void* ptr)
{
    auto int_ptr = reinterpret_cast<int*>(ptr); 
    // use int_ptr ... 
}

Then we have no aliasing problem, but still there is the std::launder case for example_2. Do we have change the callside and rewrite our example_2 function to:

void example_2()
{
    alignas(alignof(int)) char storage[sizeof(int)];
    new (&storage) int;
    f(std::launder(storage));
}

or is std::launder in function f sufficient?

Lebron answered 6/7, 2018 at 6:38 Comment(0)
H
15

The strict aliasing rule is a restriction on the type of the glvalue actually used to access an object. All that matters for the purpose of that rule are a) the actual type of the object, and b) the type of the glvalue used for the access.

The intermediate casts the pointer travels through are irrelevant, as long as they preserve the pointer value. (This goes both ways; no amount of clever casts - or laundering, for that matter - will cure a strict aliasing violation.)

f is valid as long as ptr actually points to an object of type int, assuming that it accesses that object via int_ptr without further casting.

example_1 is valid as written; the reinterpret_casts do not change the pointer value.

example_2 is invalid because it gives f a pointer that doesn't actually point to an int object (it points to the out-of-lifetime first element of the storage array). See Is there a (semantic) difference between the return value of placement new and the casted value of its operand?

Harryharsh answered 6/7, 2018 at 7:15 Comment(5)
wow the explanation for why example_2 is wrong - together with the link - was enlightening. ty! to check my understanding: if i would rewrite the call to f in example_2 to: f(reinterpret_cast<char*>(std::launder(reinterpret_cast<int*>(&storage)))); it would be valid right? the steps would be: 1. get an int*, which still points to first element of storage. 2. then launder it to get a "valid" int*. 3. then cast it to char* because the function wants char*. right?Sapp
@Lebron @Harryharsh I wonder if we can just fix at f() by auto int_ptr = std::launder( reinterpret_cast<int*>(ptr) ); as in const int h = std::launder(reinterpret_cast<Y*>(&s))->z; // OK on cppreference. Are we allow to pass around pointer to out-of-lifetime object whilst not dereferencing it?Starcrossed
@Lebron Ah, I think i found it basic.compound/3.4 invalid pointer.Starcrossed
A pointer only becomes invalid when the underlying storage is gone. Simply out-of-lifetime (due to storage reuse) is fine. Laundering in f is valid but seems questionable because it means that you pay the costs (if any) of laundering even for callers that don't need it.Harryharsh
@Harryharsh only invalid when the underlying storage is gone : please refer to standard clause. In my opinion, according to basic.life/8 : "a pointer to the new object can be obtained from a pointer that represents the address of its storage by calling std​::​launder". I guess this note implies that old pointer become invalid already because that pointer induces UB without std::launder.Starcrossed
G
0

You need not std::launder within the function f(). Being as universal as possible, the function f() might be used with any pointer: dirty (that needs to be launded) or not. It's known only on a call side, so you must use something like this:

void example_2()
{
    alignas(alignof(int)) char storage[sizeof(int)];
    new (&storage) int;        // char[] has gone, now int there
    f(std::launder(reinterpret_cast<int*>(storage)));  // assiming f(void* p)
}

And the f() itself is another story. As you mentioned, consider f() is placed in a shared library, so there are no any context assumptions at all.

Genesis answered 1/5, 2021 at 3:9 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.