Oliv
Oliv

Reputation: 18051

Object lifetime, in which situation is reused the storage?

In the C++ ISO standard N4618 (but it almost also applies to the C++11 version) one can read:

at §1.8 The C++ Object Model:

If a complete object is created (5.3.4) in storage associated with another object e of type “array of N unsigned char”, that array provides storage for the created object... [Note:If that portion of the array previously provided storage for another object, the lifetime of that object ends because its storage was reused]

=> OK, array of unsigned char can provide storage for other object, If a new object occupies storage which was previously occupied by an other object, the new objects reuses the storage of the previous.

at §3.8.8 object lifetime

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,...

=> I can construct an object at the storage location of an other object but this operation is not a "storage reuses" (Otherwise why would it be written ...before the storage which the object occupied is resused...)

And as an example of §3.8.8

struct C {
 int i;
 void f();
 const C& operator=( const C& );
};

const C& C::operator=( const C& other) {
  if ( this != &other ) {
    this->~C();          // lifetime of *this ends
    new (this) C(other); // new object of type C created
    f();                 // well-defined
  }
  return *this;
}

C c1;
C c2;
c1 = c2;  // well-defined
c1.f();  // well-defined; c1 refers to a new object of type C

So in this example new(this) C(other) would not be a storage reuse because c1 has automatic storage duration.

On the contrary in this example:

alignas(C) unsigned char a[sizeof(C)];
auto pc1 = new (&a) C{};
C c2;
*pc1 = c2;

the expression new (this) C(other) evaluated during the assignment *pc1=c2 is a storage reuse since the object pointed to by pc1 has storage provided by an unsigned char array.

Are the following assertions (and the previouses) right:

EDIT: Ok please do not focus on the "storage reuses" term and focus on the question "§3.8.8 does not applies if the initial object is constructed on a storage provided by an unsigned char array"?

Because if it is not the case, so all std::vector implementation I know are not correct. Indeed they save the allocated storage in a pointer of type value_type called __begin_ for example. Let's say you make a push_back on this vector. The object will be created at the begining of the allocated storage:

 new (__begin_) value_type(data);

Then you make a clear, which will call the destroy of the allocator which will call the destructor of the object:

 __begin_->~value_type();

Then if you make a new push_back, the vector won't allocate new storage:

new (__begin_) value_type(data);

Thus according de §3.8.8 if value_type has a ref data member or const data member, then a call to front which result in *__begin_ will not point to the new pushed object.

So I think that storage reuses has in $3.8.8 a special meaning otherwise, std library implementor are wrong? I have checked libstdc++ et libc++ (GCC and Clang).

This what would happen in this example:

 #include <vector>
 struct A{
   const int i;
 };
 int main() {
   std::vector<A> v{};
   A a{};
   v.push_back(A{});
   v.clear();
   v.push_back(A{2});
   return 0;
 }

Upvotes: 11

Views: 975

Answers (2)

curiousguy
curiousguy

Reputation: 8268

Disregarding the text of the standard, which is absolute non sense... you have to understand the meaning of it, which is cristal clear: you can pretend that a reconstructed object is the same object as the object over which it is reconstructed, if the langage semantics would have allowed such change:

struct T {
    int i;
    T (int i) :i(i) {}
    void set_i(int new_i) {
        new (this) T(new_i);
    }
};

Here set_i use a very silly way to reset member i, but note that the exact same behavior could be done by other means (assignment).

Considering

class Fixed_at_construction {
    int i;
public:
    Fixed_at_construction (int i) :i(i) {}
    int get_i() {
        return i;
    }
};

Now the value cannot be changed after construction, but only by virtue of access control: no public member allows such change. In this case, it's an invariant for the class user but not so much from a language semantics point of view (that's arguable...) and as a user you can still use placement new.

But when a member is const qualified (and not volatile qualified) or is a reference, C++ semantics imply that it cannot be changed. The value (or the referent of the reference) is the fixed at construction, and you cannot use other language feature to destroy that property. It just means that you cannot do:

class Constant {
    const int i;
public:
    Constant (int i) :i(i) {}
    void set_i(int new_i) { // destroys object
        new (this) T(new_i);
    }
};

The placement new here itself it legit, but just as much as delete this. You cannot use the object at all later: the old object is destroyed, and the name of the old object still refers to the old object, and there is no special permit to use to refer to the new one. After calling set_i, you can only use the name of the object to refer to the storage: take its address, use it as void*.

But in case of vector, object stored are not named. The class does not need to store a pointer to the objects, it only needs a pointer to the storage. v[0] happens to be a lvalue referring to the first object in the vector, it is not a name.

Upvotes: -1

Nicol Bolas
Nicol Bolas

Reputation: 473352

=> OK, array of unsigned char can provide storage for other object, If a new object occupies storage which was previously occupied by an other object, the new objects reuses the storage of the previous.

Correct, but for the wrong reasons.

The notation you cite is non-normative text. That's why it appears in the "[note: ...]" markup. Non-normative text has no weight when deciding what the standard actually says. So you cannot use that text to prove that constructing an object in an unsigned char[] constitutes storage reuse.

So if indeed it does constitute storage reuse, that is only because "reuse" is defined by plain English, not because the standard has a rule explicitly defining this as one of the cases of "storage reuse".

I can construct an object at the storage location of an other object but this operation is not a "storage reuses" (Otherwise why would it be written ...before the storage which the object occupied is resused...)

No. [basic.life]/8 is trying to explain how you can use pointers/references/variable names to an object after that object's lifetime has ended. It explains the circumstances under which those pointers/references/variable names are still valid and can access the new object created in its storage.

But let's dissect the wording:

If, after the lifetime of an object has ended

OK, so we have this situation:

auto t = new T;
t->~T(); //Lifetime has ended.

and before the storage which the object occupied is reused or released

And neither of the following has happened yet:

delete t; //Release storage. UB due to double destructor call anyway.
new(t) T; //Reuse the storage.

a new object is created at the storage location which the original object occupied

Therefore, we do this:

new(t) T; //Reuse the storage.

Now, that sounds like a contradiction, but it isn't. The "before storage gets reused" part is intending to prevent this:

auto t = new T;  //Storage created, lifetime begun.
t->~T(); //Lifetime has ended; storage not released.
new(t) T; //[basic.life]/8 applies, since storage hasn't been reused yet.
new(t) T; //[basic.life]/8 does not apply, since storage was just reused.

[basic.life]/8 is saying that the paragraph does not apply if you created a new object between the previous object's destruction and your attempt to create a new object. That is, [basic.life]/8 doesn't apply if you double reuse storage.

But the act of creating the new object is still reusing the storage. Storage reuse is not a fancy C++ term; it's just plain English. It means exactly what it sounds like: storage was used for object A, now you reuse that same storage for object B.


EDIT: Ok please do not focus on the "storage reuses" term and focus on the question "§3.8.8 does not applies if the initial object is constructed on a storage provided by an unsigned char array"?

But... it does apply.

vector stores a pointer to the first element. That object gets allocated and constructed. Then the destructor gets called, but the storage remains. Then the storage gets reused.

That is the exact case that [basic.life]/8 is talking about. The new object being created is the same type as the old one. The new object overlays the storage for the old one exactly. The objects cannot be base subobjects of anything, by the nature of vector. vector doesn't let you stick const-qualified objects in itself.

[basic.life]/8's protections very much do apply: the new object can be accessed via pointers/references to the old one. So unless you do a lot of copy/move constructor/assignment work to put types with const or reference members in vector, it will work.

And even that last case can be satisfied by implementations laundering their pointers. Oh, and launder is new, from C++17. C++14 has no provisions for what to do with types where [basic.life]/8 doesn't apply.

Upvotes: 8

Related Questions