I have been trying to understand storage reuse in C++. Imagine we have an object a with a non-trivial destructor whose storage is reused with a placement new-expression:
struct A {
~A() { std::cout << "~A()" << std::endl; }
};
struct B: A {};
A* a = new A; // lifetime of *a begins
A* b = new(a) B; // storage reuse, lifetime of *b begins
[basic.life/8] specifies:
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 original object is transparently replaceable (see below) by the new object.
Since in my example the lifetime of *a has not ended when we reuse the storage it occupies, we cannot apply that rule. So what rule describes the behavior in my case?
The applicable rule for this is laid out in §3.8 [basic.life]/p1 and 4:
The lifetime of an object of type T ends when:
if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
the storage which the object occupies is reused or released.
4 A program may end the lifetime of any object by reusing the storage
which the object occupies or by explicitly calling the destructor for
an object of a class type with a non-trivial destructor. For an object
of a class type with a non-trivial destructor, the program is not
required to call the destructor explicitly before the storage which
the object occupies is reused or released; however, if there is no
explicit call to the destructor or if a delete-expression (5.3.5) is
not used to release the storage, the destructor shall not be
implicitly called and any program that depends on the side effects
produced by the destructor has undefined behavior.
So A *b = new (a) B; reuses the storage of the A object created in the previous statement, which is well-defined behavior provided that sizeof(A) >= sizeof(B)*. That A object's lifetime has ended by virtue of its storage being reused. A's destructor is not called for that object, and if your program depends on the side effect produced by that destructor, it has undefined behavior.
The paragraph you cited, §3.8 [basic.life]/p7, governs when a pointer/reference to the original object can be reused. Since this code doesn't satisfy the criteria listed in that paragraph, you may only use a only in the limited ways permitted by §3.8 [basic.life]/p5-6, or undefined behavior results (example and footnote omitted):
5 Before the lifetime of an object has started but after the storage
which the object will occupy has been allocated or, after the lifetime
of an object has ended and before the storage which the object
occupied is reused or released, any pointer that refers to the storage
location where the object will be or was located may be used but only
in limited ways. For an object under construction or destruction, see
12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is
well-defined. Such a pointer may be dereferenced but the resulting
lvalue may only be used in limited ways, as described below. The
program has undefined behavior if:
the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a
delete-expression,
the pointer is used to access a non-static data member or call a non-static member function of the object, or
the pointer is implicitly converted (4.10) to a pointer to a base class type, or
the pointer is used as the operand of a static_cast (5.2.9) (except when the conversion is to void*, or to void* and
subsequently to char*, or unsigned char*), or
the pointer is used as the operand of a dynamic_cast (5.2.7).
6 Similarly, before the lifetime of an object has started but after
the storage which the object will occupy has been allocated or, after
the lifetime of an object has ended and before the storage which the
object occupied is reused or released, any glvalue that refers to the
original object may be used but only in limited ways. For an object
under construction or destruction, see 12.7. Otherwise, such a glvalue
refers to allocated storage (3.7.4.2), and using the properties of the
glvalue that do not depend on its value is well-defined. The program
has undefined behavior if:
an lvalue-to-rvalue conversion (4.1) is applied to such a glvalue,
the glvalue is used to access a non-static data member or call a non-static member function of the object, or
the glvalue is implicitly converted (4.10) to a reference to a base class type, or
the glvalue is used as the operand of a static_cast (5.2.9) except when the conversion is ultimately to cv char& or cv unsigned char&, or
the glvalue is used as the operand of a dynamic_cast (5.2.7) or as the operand of typeid.
* To prevent UB from cases where sizeof(B) > sizeof(A), we can rewrite A *a = new A; as char c[sizeof(A) + sizeof(B)]; A* a = new (c) A;.
There are some potential problems with this:
If B is larger than A, it will overwrite bytes not allocated - which is undefined behaviour.
Destructor of A is not called for a (or b - your code doesn't show whether you delete a or delete b or neither). This is very important if either for A or B destructor is doing something like reference counting, locks, memory deallocation (including std:: containers such as std::vector or std::string), etc.
If a is not used again after you create b, you still need to call the A destructor to make sure it's lifetime is over - see the example in the third bulled after the section you quoted. So if your purpose was to avoid the "expensive" destructor call, then your code is failing to abide by the rules given in section 3.8/7 of the standard.
You are also breaching the bullet of:
The original object was a most derived object (1.8) of type T and the new object is a most derived object of type T.
as A is not the most derived type.
In summary, "broken". Even in cases where it does work (e.g. changing to A* a = new B;), it should be discouraged, as it can lead to subtle and difficult bugs.
As an addendum, in order to do this correctly you may call the destructor explicitly.
Note: the located memory is of size B to accommodate the potential size between A and B.
Note 2: with your implementation of class A this will not work. ~A() must be made virtual!!
A *b = new B; //Lifetime of b is starting. It is important that we use `new B` rather than `new A` so as to get the correct size.
b->~B(); //lifetime of b has ended. The memory still remain allocated however.
A *a = new (a) A; //lifetime of a is starting
a->~A(); // lifetime of a has ended
// a is still allocated but in an undefined state
::operator delete(b); // release the memory allocated without calling the destructor. This is different from calling 'delete b'
I believe that calling operator delete on a base pointer should be safe. Please do correct me if this is not the case.
Alternatively, if you allocate the memory for a as a char buffer, you can then use placement new to construct A and B objects, and safely call delete[] to deallocate the buffer (since char has a trivial destructor):
char* buf = new char[sizeof(B)];
A *a = new (a) A;
a->~();
A *b = new (a) B;
b->~B();
delete[] buf;
Related
I'm wondering if the following is undefined?
int main()
{
struct Doggy { int a; ~Doggy() {} };
Doggy* p = new Doggy[100];
p[50].~Doggy();
p[50].a = 3; // Is this not allowed? The destructor was called on an
// object occupying that area of memory.
// Can I access it safely?
if (p[50].a == 3);
}
I guess this is generally good to know, but the reason I'm specifically wanting to know is that I have a data structure consisting of an array, where the buckets can be nullable by setting a value, kind of like buckets in a hash table array. And when the bucket is emptied the destructor is called, but then checking and setting the null state after the destructor is called I'm wondering if it's illegal.
To elaborate a little, say I have an array of objects and each object can be made to represent null in each bucket, such as:
struct Handle
{
int value = 0; // Zero is null value
~Handle(){}
};
int main()
{
Handle* p = new Handle[100];
// Remove object 50
p[50].~Handle();
p[50].value = 0; // Set to null
if (p[50].value == 0) ; // Then it's null, can I count on this?
// Is this defined? I'm accessing memory that was occupied by
// object that was destroyed.
}
Yes it'll be UB:
[class.dtor/19]
Once a destructor is invoked for an object, the object's lifetime ends; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended ([basic.life]).
[Example 2: If the destructor for an object with automatic storage duration is explicitly invoked, and the block is subsequently left in a manner that would ordinarily invoke implicit destruction of the object, the behavior is undefined. — end example]
p[50].~Handle(); and later delete[] p; will make it call the destructor for an object whose lifetime has ended.
For p[50].value = 0; after the lifetime of the object has ended, this applies:
[basic.life/6]
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.allocation]), and using the pointer as if the pointer were of type void* is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:
6.2 - the pointer is used to access a non-static data member or call a non-static member function of the object
Yes, it's mostly. Handle::value is just an offset to a pointer of type Handle, so it's just going to work wherever you point it to, even if the containing object isn't currently constructed. If you were to use anything with virtual keyword, this would end up broken though.
p[50].~Handle(); this however is a different beast. You should never invoke destructors manually unless you have also explicitly invoked the constructor with placement new. Still not illegal, but dangerous.
delete[] p; (omitted in your example!) is where you end up with double-destruction, at which point you are well beyond UB, straight up in the "it's broken" domain.
I recently saw a piece of code which used storage buffers to create objects and then simply swapped the buffers in order to avoid the copying overhead. Here is a simple example using integers:
std::aligned_storage_t<sizeof(int), alignof(int)> storage1;
std::aligned_storage_t<sizeof(int), alignof(int)> storage2;
new (&storage1) int(1);
new (&storage2) int(2);
std::swap(storage1, storage2);
int i1 = reinterpret_cast<int&>(storage1);
int i2 = reinterpret_cast<int&>(storage2);
//this prints 2 1
std::cout << i1 << " " << i2 << std::endl;
This feels like undefined behaviour in the general case (specifically swapping the buffers and then accessing the objects as if they were still there) but I am not sure what the standard says about such usage of storage and placement new. Any feedback is much appreciated!
I suspect there are a few factors rendering this undefined, but we only need one:
[C++11: 3.8/1]: [..] The lifetime of an object of type T ends when:
if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
the storage which the object occupies is reused or released.
All subsequent use is use after end-of-life, which is bad and wrong.
The key is that each buffer is being reused.
So, although I would expect this to work in practice at least for trivial types (and for some classes), it's undefined.
The following may have been able to save you:
[C++11: 3.8/7]: 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 [..]
…except that you are not creating a new object.
It may or may not be worth noting here that, surprisingly, the ensuing implicit destructor calls are both well-defined:
[C++11: 3.8/8]: If a program ends the lifetime of an object of type T with static (3.7.1), thread (3.7.2), or automatic (3.7.3) storage duration and if T has a non-trivial destructor, the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined.
An object is not created when we use malloc/free for a class in c++. Then why does this code works?
If an object is not created then it must not give the output mentioned below.
class Test
{
public:
Test()
{
cout << "Test : ctor\r\n";
}
~Test()
{
cout << "Test : dtor\r\n";
}
void Hello()
{
cout << "Test : Hello World\r\n";
}
};
int main()
{
cout << "2\n";
Test* t2 = (Test*) malloc(sizeof Test);
t2->Hello();
free(t2);
return 0;
}
OUTPUT:
Hello World
Undefined Behavior is the short answer. The long answer is that because your class has no virtual methods calling them directly is just a function call with an implicit this parameter that points to the memory you've allocated. That said if any of your methods were to access this it would cause more undefined behavior because the object hasn't been constructed.
blatantly stolen from Columbo and LRIO:
[C++11: 3.8/1]: The lifetime of an object is a runtime property of the object. An object is said to have non-trivial initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor. [ Note: initialization by a trivial copy/move constructor is non-trivial initialization. —end note ]
The critical bit there is the it the pointer to the object can contain data other than just a place where the members are stored. It is up to the implementation to decide what that is.
An object is not created when we use malloc/free for a class in c++. Then why does this code works?
It depends on how you define "works", an incredibly vague term that you should generally avoid in questions like this.
I would consider that the code doesn't "work": it has a bug. It is broken. On any given run of execution it could fail. It could crash. It could open a wormhole.
What is the bug? The bug is that you are invoking undefined behaviour. You allocated memory for a Test object, but you never created a Test object to put there. You then perform operations on the memory as if it contained a Test object, which it doesn't.
[C++11: 12.1/5]: [..] A default constructor is trivial if it is not user-provided [..] Otherwise, the default constructor is non-trivial.
[C++11: 3.8/1]: The lifetime of an object is a runtime property of the object. An object is said to have non-trivial initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor. [ Note: initialization by a trivial copy/move constructor is non-trivial initialization. —end note ] The lifetime of an object of type T begins when:
storage with the proper alignment and size for type T is obtained, and
if the object has non-trivial initialization, its initialization is complete.
The lifetime of an object of type T ends when:
if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
the storage which the object occupies is reused or released.
[C++11: 3.8/3]: The properties ascribed to objects throughout this International Standard apply for a given object only during its lifetime. [..]
[C++11: 3.8/5]: Before the lifetime of an object has started but after the storage which the object will occupy has been allocated [..] any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. [..] Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:
[..]
the pointer is used to access a non-static data member or call a non-static member function of the object
[..]
What you are seeing is chance success: the false appearance that your program does not have a bug. It is caused by the fact that your computer does not execute C++ programs, but machine code, and sometimes during the translation from the C++ abstraction to machine code, the breaking of preconditions is lost.
More specifically, your computer doesn't care that your program is broken in this particular way, because it doesn't need to do anything that relies on your object existing. Those member functions do not touch any instance data so your computer never realises that the code is wrong.
That doesn't mean the code "works".
All quotes from the C++11 FD.
Your code is almost fine. The problem is that your class has non-trivial initialization due to it having a user-provided and thus non-trivial default constructor - [basic.life]/1:
An object is said to have non-trivial initialization if it is of a
class or aggregate type and it [..] is initialized by
a constructor other than a trivial default constructor.
Thus the object isn't alive throughout the program, which is clarified right after the above quote:
The lifetime of an object of type T begins when:
storage with the
proper alignment and size for type T is obtained, and
if the object
has non-trivial initialization, its initialization is complete.
... hence UB according to [basic.life]/5:
Before the lifetime of an object has started but after the storage
which the object will occupy has been allocated [..] any pointer that
refers to the storage location where the object will be or was located
may be used but only in limited ways. [..] Otherwise, such a pointer
refers to allocated storage (3.7.4.2), and using the pointer as if the
pointer were of type void*, is well-defined. Such a pointer may be
dereferenced but the resulting lvalue may only be used in limited
ways, as described below. The program has undefined behavior if:
the pointer is used to access a non-static data member or call a
non-static member function of the object, or
Is it correct to do the following :
MyClass mc1(1, 1);
MyClass* pmc1 = &mc1;
delete pmc1;
I saw it in a code example. I thought we can call delete on pointers to object that is allocated on the heap but this pointer points to an object on the stack, isn't it ?
Could someone explain it.
No. You should never call delete on a pointer to the object that hasn't been created using new. It is undefined behavior.
C++ Standard n3337 § 5.3.5/2 Delete
(...)the value of the operand of delete may be a null pointer value, a
pointer to a non-array object created by a previous new-expression, or
a pointer to a subobject (1.8) representing a base class of such an
object (Clause 10). If not, the behavior is undefined.
Is the following legal in C++?
As far as I can tell, Reference has a trivial destructor, so it should be legal.
But I thought references can't be rebound legally... can they?
template<class T>
struct Reference
{
T &r;
Reference(T &r) : r(r) { }
};
int main()
{
int x = 5, y = 6;
Reference<int> r(x);
new (&r) Reference<int>(y);
}
You aren't rebinding a reference, you're creating a new object in the memory of another one with a placement new. Since the destructor of the old object was never run I think this would be undefined behavior.
There is no reference being rebound in your example. The first reference (constructed on line two with the name r.r) is bound to the int denoted by x for the entire of its lifetime. This reference's lifetime is ended when the storage for its containing object is re-used by the placement new expression on line three. The replacement object contains a reference which is bound y for its entire lifetime which lasts until the end of its scope - the end of main.
I think I found the answer in a passage below the "quoted" one that talks about trivial dtor / dtor side effects, namely [basic.life]/7:
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
the original object was a most derived object of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).
By reusing the storage, we end the lifetime of original object [basic.life]/1
The lifetime of an object of type T ends when:
if T is a class type with a non-trivial destructor, the destructor call starts, or
the storage which the object occupies is reused or released.
So I think [basic.life]/7 covers the situation
Reference<int> r(x);
new (&r) Reference<int>(y);
where we end the lifetime of the object denoted by r, and create a new object at the same location.
As Reference<int> is a class type with a reference data member, the requirements of [basic.life]/7 are not fulfilled. That is, r might not even refer to the new object, and we may not use it to "manipulate" this newly created object (I interpret this "manipulate" also as read-only accesses).