5

I was reading cppreference and in the example for std::aligned_storage is had this example of a vector/array class:

template<class T, std::size_t N>
class static_vector
{
    // properly aligned uninitialized storage for N T's
    typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
    // IF you want to see possible implementation of aligned storage see link.
    // It's very simple and small, it's just a buffer
    
    std::size_t m_size = 0;
 
public:
    // Create an object in aligned storage
    template<typename ...Args> void emplace_back(Args&&... args) 
    {
        if( m_size >= N ) // possible error handling
            throw std::bad_alloc{};
 
        // construct value in memory of aligned storage
        // using inplace operator new
        new(&data[m_size]) T(std::forward<Args>(args)...);
        ++m_size;
    }
 
    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const 
    {
        // note: needs std::launder as of C++17
        // WHY
        return *reinterpret_cast<const T*>(&data[pos]);
    }
 
    // Delete objects from aligned storage
    ~static_vector() 
    {
        for(std::size_t pos = 0; pos < m_size; ++pos) {
            // note: needs std::launder as of C++17
            // WHY?
            reinterpret_cast<T*>(&data[pos])->~T();
        }
    }
};

Essentially each bucket/area/address of memory where each element resides is a char buffer. At each element position that exists a placement new is done in that buffer of the T type. So that when returning the position of that buffer that char buffer can be cast to T* because a T was constructed there. Why is std::launder required from C++17 onwards? I have asked a bunch of questions about this and it seems agreed that:

alignas(alignof(float)) char buffer [sizeof(float)];
new (buffer) float;// Construct a float at memory, float's lifetime begins
float* pToFloat = buffer; // This is not a strict aliasing violation
*pToFloat = 7; // Float is live here

This is not a strict aliasing violation apparently, it's completely fine. So therefore why is std::launder required here?

Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • 1
    You must use the pointer returned by `new` to access the created object. Casting `buffer` is not allowed. `std::launder` can be used to obtain a valid pointer from `buffer` though. – spectras Aug 21 '21 at 01:18
  • 2
    As a side note, the comment is somewhat misleading. The use of std::launder is required to make the code valid. The required tool was just missing before c++17, making such code impossible to write legally. – spectras Aug 21 '21 at 01:21
  • @spectras So the following has always been illegal in C++? char buffer[32]; new (&buffer[0]) MyStruct{}; (*((MyStruct *)&buffer[0])).~MyStruct(); ??? Construct an object in a char buffer, and call the destructor? – Zebrafish Aug 21 '21 at 01:28
  • 1
    Yes. `char buffer[32]; MyStruct * ptr = new (&buffer[0]) MyStruct{}; ptr->~MyStruct();` is fine though (assuming alignment and size requirements are fulfilled). – spectras Aug 21 '21 at 01:29
  • @spectras So before c++17 if you're constructing a vector class, and you have a char array buffer member, and you go about constructing Ts in place, before std::launder how could you construct such a vector class? You would return the position in the buffer cast as T, but this is apparently illegal without std::launder. – Zebrafish Aug 21 '21 at 01:32
  • 1
    Zebrafish> Yes, std::launder was added precisely to have a legal way to write this, instead of relying on undefined behavior and hoping the compiler will do the right thing. – spectras Aug 21 '21 at 01:33
  • @spectras Well it's interesting that on that page it says "// note: needs std::launder as of C++17", as if to say before C++17 you didn't need std::launder and things would work. Doesn't seem to imply that the entire vector class couldn't be written before C++17, or as if to say from C++17 onwards you need std::launder for it to be defined behavior. – Zebrafish Aug 21 '21 at 01:36
  • 1
    That is precisely what I mentioned in my second comment, above: that note is misleading. Before C++17 there was no legal way to write that code. – spectras Aug 21 '21 at 01:42
  • @spectras: From what I can tell, accessing storage which was created using placement new, using a pointer which was returned from placement new, is no guarantee that clang and gcc will recognize the pointer as identifying an object of the new type. Consider https://godbolt.org/z/PTTcG4T6q if `test()` is passed a pointer to a `long long` worth of storage along with `i`, `j`, and `k` values of zero. Can the rules be considered workable if neither clang nor gcc seems to understand them? – supercat Aug 27 '21 at 19:40
  • @supercat It looks like you found a corner case where they fail to account for the possibility `i==k`. You don't even need all the functions, you can just make it happen [like this](https://godbolt.org/z/a6Eofeoq3). I do not understand how where you think this is related to the topic? – spectras Aug 28 '21 at 10:18
  • @spectras: I think I mean to respond to François Andrieux's point. – supercat Aug 28 '21 at 16:26
  • @FrançoisAndrieux: Neither clang nor gcc reliably handle all corner cases where code uses pointers which are *directly received* from placement new. I keep hearing that languages need to have rules that are difficult for programmers to work with in order to allow optimizers to handle them, but if the rules were actually workable, optimization bugs should be rare, and bugs common to both clang and gcc even moreso (see godbolt link in my earlier comment to spectras). I think programmers expect compilers to seek to process a dialect which, in exchange for missing a few optimizations, ... – supercat Aug 28 '21 at 16:39
  • @supercat I've had another look at `launder` since posting that comment, and I now think that it should be okay, given the assumption made. – François Andrieux Aug 28 '21 at 16:43
  • ...is actually workable for programmers and compilers alike. In fact, a compiler's willingness to forego a few dubious optimizations would make it safe for programmers to use Whole Program Optimization, for which I otherwise cannot see any safe use (each of the functions in my example would easily be processed correctly in isolation, but there's no way anyone who writes such functions could imagine all the ways they might interact if invoked from someone else's code). – supercat Aug 28 '21 at 16:44
  • @FrançoisAndrieux: Would `launder` fix my posted example, and if so where and how should it be used? From what I can tell, the Standard doesn't require laundering storage occupied by a standard-layout type before passing it to placement new, and the code only accesses pointers received directly from placement new, rather than separately-derived pointers to the same objects. – supercat Aug 28 '21 at 16:46
  • @supercat I don't see a reason for using `std::launder` in your example. My only recommendation would be using `std::aligned_storage` instead of `char buffer[32]`. – François Andrieux Aug 28 '21 at 17:26
  • @FrançoisAndrieux: I was referring to example https://godbolt.org/z/PTTcG4T6q which receives a `void*` and three integers from outside code. I think both clang and gcc are assuming that because it can tell that `pll[k]` and `pl2[k]` have the same address, it may treat them interchangeably and replace `pl2[k]` with `pll[k]`, thus allowing the writes to `pll[k]` to be elided. I think a sound abstraction model designed to facilitate optimization would need to consider pointer provenance, but in terms of sequencing relationships. If an action which forms a pointer by unknown means... – supercat Aug 28 '21 at 17:45
  • ...occurs after a pointer that "leaks" a pointer, operations on the former pointer which preceded the leak should be sequenced before actions on the latter pointer which follow its synthesis, even if a compiler can't prove the synthesized pointer matches the leaked one. An abstraction model based on sequencing would be easier for both programmers and compilers to reason about, but I think the maintainers of clang and gcc are attached to the idea that it will be possible to fix the bugs that result from their present abstraction model. – supercat Aug 28 '21 at 17:48
  • @supercat It is hard to say anything about that example without seeing what the arguments of `test` are. – François Andrieux Aug 28 '21 at 21:12
  • @supercat It looks mostly okay as long as `p` points to storage large enough and aligned for a `long long` to fit, and if `i`, `j` and `k` are 0. Anything else would be Undefined Behavior. The compiler is allowed to assume these are true and optimize accordingly, greatly simplifying the generated code. – François Andrieux Aug 28 '21 at 21:20
  • @FrançoisAndrieux: Neither clang nor gcc generates correct code for the case where i, j, and k are all zero. From what I call tell, both of them see a sequence of actions which reads an address (`pll[k]`), stores 2 to that address (using lvalue `pl2[k]`), stores the previously-read value to that address (again, using `pl2[k]`), and then discards the read value, as a no-op and omit it entirely, thus losing any connection between the read-and-rewritten value and `pl2[0]`. – supercat Aug 28 '21 at 21:35
  • @supercat The generated code looks correct to me. The function returns `2` in the only non-UB path, so it is correct for the compiler to generate assembly that always returns `2`. – François Andrieux Aug 28 '21 at 21:40
  • @FrançoisAndrieux: The return value is passed in EAX. The link I posted shows the clang assembly code which ends with "mov eax,1". Switching to gcc and looking at its assembly code, it does read back `pl2[i]`, but it performs that read before the last write of `pl2[k]`. If you write code that calls this function in such a way that the compiler can substitute 0 for i, j, and k, then the code will happen to work, but behavior would be defined in cases where i, j, and k are all zero regardless of whether the compiler can prove that they would be. – supercat Aug 29 '21 at 01:05
  • @FrançoisAndrieux: If you want to properly test executable examples in godbolt, make function calls through `volatile` pointers to test the way functions would behave if a compiler knows nothing about the caller. I find it easier for the most part, especially in simple cases, to simply look at the assembly-language output and see when and low EAX gets its value before the function returns. – supercat Aug 29 '21 at 01:07
  • @supercat I missed that. I'm not sure what causes the problem, but it looks like a strict aliasing violation, though I can't pinpoint the error. The compiler can assume a pointer to `long` is never also a pointer to a `long long`, so changes to a `long long` can't possible change a `long`. But this example is not related to the question, and we should stop discussing it in this comment section. Feel free to post a question about it. – François Andrieux Aug 29 '21 at 03:13
  • @supercat Might be a compiler bug. You get the right result if you make `test` a `static` function, allowing it to be fully inline. – François Andrieux Aug 29 '21 at 03:20
  • @FrançoisAndrieux: If both clang and gcc behave in the same fashion contrary to what the Standard would seem to require, is that a "bug", or is that a result of the authors of clang and gcc deciding that conflicts between their abstraction model and the Standard are a result of defects in the Standard? I think it's the latter, which in some scenarios might be fine (since upholding the Standard in all corner cases would needlessly inhibit some optimizations that would otherwise be useful) but undermines the usefulness of discussing the Standard's corner cases. – supercat Aug 30 '21 at 16:14
  • @supercat It is only a standard defect if it isn't self-consistent or if the wording conflicts with the committee's intentions. I don't see either here. It's either a compiler bug or there is UB in the code that neither one of us has spotted. I suspect the latter case. I won't be responding to this comment chain anymore, please post a question if you still have a question. – François Andrieux Aug 30 '21 at 17:43

1 Answers1

1
    // note: needs std::launder as of C++17
    return *reinterpret_cast<const T*>(&data[pos]);

Why is std::launder required from C++17 onwards?

The conditions through which you can dereference a pointer from reinterpret_cast are minimal. This is not one of them. You are casting a pointer to one type to a pointer to a completely unrelated type, and reading through it. See here.

std::launder allows you to derive a valid pointer. It's whole reason for existence is to give you this.

Prior to C++17, you would have had to save the pointer returned from placement new, as that was the only valid pointer to T in sight, and before std::launder you could not make one from the buffer.

I have asked a bunch of questions about this and it seems agreed that... This is not a strict aliasing violation apparently, it's completely fine. So therefore why is std::launder required here?

That is not fine, for the same reason. Nor is it agreed that it's fine. See @eerorika's answer on your question here.

Jeff Garrett
  • 5,863
  • 1
  • 13
  • 12
  • If it's true that the only way an object can be accessed and written to in dynamically allocated memory is by using placement new, then that means that any code that uses malloc for an int buffer without placement new is undefined. And also that before C++ had placement new one couldn't use malloc as a buffer or int, or any other type really? – Zebrafish Aug 30 '21 at 05:32
  • @Zebrafish placement new was there from the first full version of C++. Before that it was not, but that was a time where you had to assign `this` manually in your constructor, too. – spectras Aug 30 '21 at 08:25
  • Also, there is a [special rule](https://eel.is/c++draft/basic.memobj#intro.object-10) for *implicit-lifetime types* that allows the compiler to implicitly start the lifetime of objects that fulfill specific conditions. Scalar types fulfill those conditions, so simply casting the result of malloc for a scalar array is well-defined. Only the first time though, don't store the void* and cast it again. – spectras Aug 30 '21 at 08:28
  • @spectras "implicit-lifetime types". Since when? Are you talking about the new proposal? http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0593r5.html ? Do you see in that link, about a page or two down, it mallocs and assigns to a struct X pointer, then accesses its member. It says that though this is idiomatic, it's undefined in C++. However from the cppreference page struct X should be an "aggregate type", which is an "implicit lifetime" type. Could you clarify this? – Zebrafish Aug 30 '21 at 08:36
  • @Zebrafish Yes it is a recent addition, maybe as recent as C++20 - I did not follow whether it stemmed from this specific PR or not. Before that you needed to placement-new into malloc-created storage. – spectras Aug 30 '21 at 08:44
  • @JeffGarrett Given that some types are implicit lifetime objects, then you can malloc and alias that memory with another pointer type, without assigning the pointer to the result of a placement new, right? – Zebrafish Aug 30 '21 at 08:54
  • This is one of the more arcane areas of C++. Prior to P0593, yes, one had to use placement new to start the lifetime of an object in storage from malloc. Otherwise, there was no object. – Jeff Garrett Aug 30 '21 at 14:26
  • By the way, a better link for P0593 is https://wg21.link/p0593 (revision 6 was the last). – Jeff Garrett Aug 30 '21 at 14:26
  • The point of P0593 was to make this defined for simple cases. I somehow got the impression the core language part had been accepted as a defect report, which compilers usually apply even in previous language standard modes. I don't have compelling evidence for that. – Jeff Garrett Aug 30 '21 at 14:26
  • "Given that some types are implicit lifetime objects, then you can malloc and alias that memory with another pointer type, without assigning the pointer to the result of a placement new, right?" First of all, I'd note that implicit lifetime is an unsuitable restriction for a generic parameter. There is no way to assert or require that T is an implicit lifetime type. And implicit lifetime is not recursive (members may not be implicit lifetime types). But you could say, I only ever instantiate this with T a fundamental type... – Jeff Garrett Aug 30 '21 at 14:50
  • Implicit lifetime types give you that an object exists. You also need to get a pointer to it. That's what placement new gives you. https://eel.is/c++draft/intro.object#11 and what reinterpret_cast does not https://eel.is/c++draft/basic.compound#4 – Jeff Garrett Aug 30 '21 at 14:55