On std::launder, GCC and clang: why such a different behavior?

136 views Asked by At

I was tinkering with the example given on the cppreference launder web page.

The example shown below suggest that either I misunderstood something and introduced UB or that there is a bug somewhere or that clang is to lax or too good.

  1. In doit1(), I believe the optimization done by GCC is incorrect (the function returns 2) and does not take into account the fact that we use the placement new return value.
  2. In doit2(), I believe the code is also legal but with GCC, no code is produced ?

In both situations, clang provides the behavior I expect. On GCC, it will depend on the optimization level. I tried GCC 12.1 but this is not the only GCC version showing this behavior.

#include <new>

struct A {
    virtual A* transmogrify(int& i);
};

struct B : A {
    A* transmogrify(int& i) override {
        i = 2;
        return new (this) A;
    }
};

A* A::transmogrify(int& i) {
    i = 1;
    return new (this) B;
}

static_assert(sizeof(B) == sizeof(A), "");

int doit1() {
    A i;
    int n;
    int m;

    A* b_ptr = i.transmogrify(n);

    // std::launder(&i)->transmogrify(m);    // OK, launder is NOT redundant
    // std::launder(b_ptr)->transmogrify(m); // OK, launder IS     redundant
                   (b_ptr)->transmogrify(m); // KO, launder IS redundant, we use the return value of placment new

    return m + n; // 3 expected, OK == 3, else KO
}

int doit2() {
    A i;
    int n;
    int m;

    A* b_ptr = i.transmogrify(n);

    // b_ptr->transmogrify(m); // KO, as shown in doit1
    static_cast<B*>(b_ptr)->transmogrify(m); // VERY KO see the ASM, but we realy do have a B in the memory pointed by b_ptr

    return m + n; // 3 expected, OK == 3, else KO
}

int main() {
    return doit1();
    // return doit2();
}

Code available at: https://godbolt.org/z/43ebKf1q6

1

There are 1 answers

1
Artyer On

The UB comes from accessing A i; to call the destructor at the end of scope without laundering the pointer. That can let the compiler assume i has not been destroyed by the storage reuse before then.

You need something more like:

    alignas(B) std::byte storage[sizeof(B)];
    A& i = *new (storage) A;

    // ...

    static_cast<B*>(std::launder(&i))->~B();
    // or: b_ptr->~B();
    // or: simply don't call the destructor