7

As explained in P0532R0, in the folowing use case std::launder must be used to avoid undefined behavior (UB):

struct X{
  const int i;
  x(int i):i{i}{}
  };

unsigned char buff[64];
auto p = new(buff) X(33);
p->~X();
new(buff) X(42);
p = std::launder(p);
assert(p->i==42);

But what happen in the case where more than one object is on the buffer (this is exactly what would happen if one pushes 2 X in a vector, clears the vector and then pushes two new X):

unsigned char buff[64];
auto p0 = new(buff) X(33);
auto p1 = new(p0+1) X(34);
p1->~X();
p0->~X();
new(buff) X(42);
new(p0+1) X(43);
p0 = std::launder(p0);
assert(p0->i==42);
assert(p0[1].i==43);//???

Is the last assertion correct, or p0[1] still invokes UB?

yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
Oliv
  • 17,610
  • 1
  • 29
  • 72

2 Answers2

6

Your code invokes UB, but not for launder reasons. It's because p0[1].i is itself UB.

Yes, really ([Expr.Add]/4):

When an expression that has integral type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j ) point to the (possibly-hypothetical) element x[i + j] if 0 ≤ i + j ≤ n ; otherwise, the behavior is undefined. Likewise, the expression P - J points to the (possibly-hypothetical) element x[i − j] if 0 ≤ i − j ≤ n; otherwise, the behavior is undefined.

An object that is not an array element is considered to belong to a single-element array for this purpose; see 8.3.1. A pointer past the last element of an array x of n elements is considered to be equivalent to a pointer to a hypothetical element x[n] for this purpose; see 6.9.2.

[] when applied to a pointer means to do pointer arithmetic. And in the C++ object model, pointer arithmetic can only be used on pointers to elements in an array of the type being pointed to. You can always treat an object as an array of length 1, so you can get a pointer to "one past the end" of the single object. Thus, p0 + 1 is valid.

What is not valid is accessing the object stored at that address though the pointer obtained via p0 + 1. That is, p0[1].i is undefined behavior. This is just as UB before laundering it as after.

Now, let's look at a different possibility:

X x[2];
x[1].~X(); //Destroy this object.
new(x + 1) X; //Construct a new one.

So let's ask some questions:

Is x[1] UB? I would say... no, it is not UB. Why? Because x[1] is not:

a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object

x points to the array and the first element of that array, not the second element. Therefore, it does not point to the original object. It is not a reference, nor is it the name of that object.

Therefore, it does not qualify for the restrictions stated by [basic.life]/8. So x[1] should point to the newly constructed object.

Given that, you don't need launder at all.

So if you're doing this in a way that's legal, then you don't need launder here.

Community
  • 1
  • 1
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Wait a second. Does that mean that doing pointer arithmetic on pointer returned by `std::vector::data()` is UB too? As it essentually an example 1: a collection of objects created in some internal originally uninitialized buffer. – Revolver_Ocelot Sep 02 '17 at 16:45
  • 1
    @Revolver_Ocelot: No, doing pointer arithmetic on what `vector` returns is fine. But that's because the standard *explicitly specifies* that it is fine. `vector` is part of the standard library and is therefore allowed to do implementation-defined things that would be UB for users. In short, this rule means that you cannot legally implement `vector` yourself. – Nicol Bolas Sep 02 '17 at 16:53
  • According to this reasoning, is `(p + 1)[-1]` UB in the first case? – Passer By Sep 02 '17 at 20:07
  • @PasserBy: That's equivalent to `*((p + 1) - 1)`, which is fine. – Nicol Bolas Sep 02 '17 at 20:34
  • @NicolBolas After reading [intro.object] it looks like I could get a work around to the pointer arithmetic limitation you pointed out using this ugly thing:`(*reinterpret_cast(reinterpret_cast(p0)+sizeof(X)))`. Is there no other work around? May this trick confuse the optimizer? – Oliv Sep 02 '17 at 20:41
  • Hmm, `x[1]` is equivalent - by definition - to `*(x + 1)`, and `x + 1` in turn, by the definition of pointer arithmetic, points to the second element of the array `x`. Now, does that pointer point to the out-of-lifetime original element, or the in-lifetime new object? – T.C. Sep 02 '17 at 23:41
  • @T.C. The expression `x + 1` is not a pointer to the original object since that object no longer exists. You can only get a pointer to the original object by already having one before it is destroyed. – Nicol Bolas Sep 03 '17 at 01:20
  • `X x; x.~X(); &x;` gets a pointer to the no-longer-exists object `x` after its destruction, so I don't see how "that object no longer exists" means `x + 1` can't possibly be pointing to it. I'm also not seeing anything that says that the new object created by the placement new is, in fact, the second element of the array. – T.C. Sep 03 '17 at 01:46
  • @T.C.: `x` is "the name of the original object". And therefore it counts. – Nicol Bolas Sep 03 '17 at 02:47
  • I actually meant, does the arithmetic effectively launder the pointer since it is neither a pointer to the original object or the name of the object – Passer By Sep 03 '17 at 08:37
2

The reason std::launder is needed in the first place is due to this violation from [basic.life]

If, after 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, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if: [...]

  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and [...]

Hence without std::launder, p the original pointer would not point to the newly constructed object.

If these conditions are not met, a pointer to the new object can be obtained from a pointer that represents the address of its storage by calling std​::​launder

Which is why std::launder does what it does.

From [ptr.launder], aptly titled Pointer optimization barrier

If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless the type contains const or reference members; in the latter cases, this function can be used to obtain a usable pointer to the new object.

Which is saying the original pointer cannot be used to refer to the newly constructed object unless laundered.

From what I can see, it can be interpreted both ways (might be entirely mistaken).

  • A pointer computed from a laundered pointer is not the original pointer, so its well-formed
  • Nowhere is it mentioned that a pointer computed from a laundered pointer is laundered, so its UB

I personally believe the first to be true, due to std::launder being a pointer optimization barrier.

Community
  • 1
  • 1
Passer By
  • 19,325
  • 6
  • 49
  • 96