4

Consider the following example:

#include <iostream>

struct A {

    int i;

    A(int i)
    {
        this->i = i;
    }

    A &operator=(const A &a) = delete;
    A(const A &a) = delete;
};

int main()
{
    A a(1);
    new(&a) A(5);
    //a = A(7); // not allowed since = is deleted in A
    std::cout << a.i << std::endl;
}

This is a simple example using the placement new operator. Since the copy constructor and assignment operator of struct A have been deleted (for whatever reason), it is not possible to change the object the variable A a holds, except for passing its address to the placement new operator.

Reasons for this might include that struct A holds large arrays (e.g. 100M entries) which would have to be copied in the assignment operator and the copy constructor.

The first part of the question revolves around the "legality" of this approach. I found this stackoverflow question, the accepted answer of which says

this is perfectly legal. And useless, because you cannot use var [A a in this case] to refer to the state of the [object] you stored within it after the placement new. Any such access is undefined behavior. […] under no circumstance may you ever refer to var after you placement new'd over it.

Why would that be the case? I have seen several other examples for the placement new operator, which are always similar to

A a(1);
A *b = new(&a) A(2);
// Now use *b instead of a

From my understanding it should not matter whether A a or A *b is used to access the object since the placement new replaces the object at the address of A a which of course is A a. That is, I would expect that always b == &a. Maybe the answer was not clear enough and this limitation is due to the const-ness of the class member.

Here is another example with the same idea, however this time struct A is embedded into another object:

#include <iostream>

struct A {

    int *p;

    A(int i)
    {
        p = new int(i);
    }

    ~A()
    {
        delete p;
    }

    A &operator=(const A &a) = delete;
    A(const A &a) = delete;
};

struct B {

    A a;

    B(int i) : a(i)
    {
    }

    void set(int i)
    {
        a.~A(); // Destroy the old object
        new(&a) A(i);
    } 

};

int main()
{
    B b(1);
    b.set(2);
    std::cout << *(b.a.i) << std::endl;
    // This should print 2 and there should be no memory leaks
}

The question is basically the same with the same reasoning. Is it valid to placement-new into the address &a?

HerpDerpington
  • 3,751
  • 4
  • 27
  • 43
  • 2
    `a` and `b` may refer to the same address and have the same type but that does not mean they refer to the same object *in terms of the abstract machine*. `a` no longer exists, `b` points to what replaced it. – François Andrieux Oct 07 '19 at 18:14
  • 2
    In this example, why not just `a.i = 5;`? You are only preventing copies from made, not preventing altering the state of the original object. In the text of your question, you also conflate the **address** of A with A itself. – sweenish Oct 07 '19 at 18:15
  • @sweenish That would work in this example, but only if the members are public. – HerpDerpington Oct 07 '19 at 18:17
  • Which they are, so why not do that? I only have the code you provided to go off of. – sweenish Oct 07 '19 at 18:18
  • 2
    @sweenish I think that's immaterial to the question. This question is not asking how to work around an existing problem. It's asking about the rules and nature of the language. The code is only to illustrate the question. – François Andrieux Oct 07 '19 at 18:18
  • @FrançoisAndrieux I get what you're saying, but then the example code is still bad. I don't think making the data member private is too high a cost on OP. Per my first comment, only copies are being prevented. I believe moves have also been implicitly deleted. Is the class supposed to be on lockdown? If so, what valid reason would you have for breaking it in the first place? I can appreciate the deep dive into the language, but I also find it pointless unless a valid use case is presented. Otherwise, it's not even academic. It's just hacks and bad practice/engineering. – sweenish Oct 07 '19 at 18:24
  • 1
    @sweenish Academic specifically means "not of practical relevance; of only theoretical interest". Requiring a practical example to qualify as academic is a contradiction. And being informed on formal details of a language is the opposite of bad engineering. If everyone who thought of a bad hack first took the time to understand and consider the consequences of their actions, I think the world of software development would have far fewer problems. – François Andrieux Oct 07 '19 at 18:30
  • 1
    I'm not taking issue with your actual work flow. But this comment just makes me think that move construction and assignment might be all you really need. Keep the copy stuff nuked. The wholesale replacement against the wishes of the class API is asking for trouble, especially if your class holds anything on the heap. – sweenish Oct 07 '19 at 18:59
  • @sweenish Could you point me to some example how this might be implemented in the second case (i.e. the variable moved to is inside another class)? – HerpDerpington Oct 07 '19 at 19:42

1 Answers1

4

For this specific code, you are okay and can use a to refer to the new object that you put in its place. This is covered by [basic.life]/8

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 storage for the new object exactly overlays the storage location which the original object occupied, and

  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

  • 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

  • neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).

emphasis mine

You check off all of those requirements so a will refer to the "new" A that you placed in a's memory.

Community
  • 1
  • 1
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 2
    @FrançoisAndrieux `std::launder` is needed when the pointer you have is no longer valid. That could be because you put a different object type in its place or constructing a new object on top of a const data. Then you need launder as the pointer to the old object is basically invalid to use anymore. – NathanOliver Oct 07 '19 at 18:37
  • 1
    If I understand correctly, if you respect the rules you enumerate in your answer, pointers that pointed to the old `a` now point to the new `a` and you don't need to `std::launder`. But if you don't respect these rules, those pointers are invalidated and you need `std::launder`. But then `std::launder` only works if the actual address of the old object is the same as the address of the new object and it's not clear to me when this is guaranteed if you already assume the mentioned rules are not respected. But now that I've reworded it, I realize that it's out of scope for this answer. – François Andrieux Oct 07 '19 at 18:40
  • Does the second case (class type) from the third bullet point mean that if `struct A { int &i; /* … */ } /* … */` this usage would be invalid? – HerpDerpington Oct 07 '19 at 18:42
  • 1
    @HerpDerpington Correct. If there was a reference member then it would no longer be valid to refer to the new `A` using `a`. This is part of the reason you should just capture the the pointer returned by `new` and use that. The rules are complex and instead of having to remember them all, you can just remember to use `auto new_ptr = new(&a) A(5);` and the just use `new_ptr`. – NathanOliver Oct 07 '19 at 18:44
  • 1
    @FrançoisAndrieux IIRC, placement new always returns the same address. That said, a new question asking about this would be nice to have, or maybe we already do. I haven't tried searching for that information yet. – NathanOliver Oct 07 '19 at 18:46