Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How C++ placement new works?

This question is to confirm I understood the concept right and take expert opinion on the style of usages and possible optimization.

I am trying to understand "placement new" and following is the program I came up with...

 #include <iostream>
 #include <new>

 class A {
 int *_a;
 public:
 A(int v) {std::cout<<"A c'tor clalled\n";_a= new int(v);}
 ~A() {std::cout<<"A d'tor clalled\n"; delete(_a);}
 void testFunction() {std::cout<<"I am a test function &_a = "<<_a<<" a = "<<*_a<<"\n";}
};
int main()
{
    A *obj1 = new A(21);
    std::cout<<"Object allocated at "<<obj1<<std::endl;
    obj1->~A();
    std::cout<<"Object allocated at "<<obj1<<std::endl;
    obj1->testFunction();
    A *obj2 = new(obj1) A(22);
    obj1->testFunction();
    obj2->testFunction();
    delete(obj1);// Is it really needed now? Here it will delete both objects.. so this is not the right place.
    //obj1->testFunction();
    //obj2->testFunction();
    return 0;
}

When I run this program I get following o/p

A c'tor clalled
Object allocated at 0x7f83eb404c30
A d'tor clalled
Object allocated at 0x7f83eb404c30
I am a test function &_a = 0x7f83eb404c40 a = 21
A c'tor clalled
I am a test function &_a = 0x7f83eb404c40 a = 22
I am a test function &_a = 0x7f83eb404c40 a = 22
A d'tor clalled
I am a test function &_a = 0x7f83eb404c40 a = 0
I am a test function &_a = 0x7f83eb404c40 a = 0

I have the following question...

  • Is it a correct example to demonstrate placement new?
  • member a is dynamically allocated (with no placement new). So why it is getting the same address for obj1 & obj2. Is it just a coincidence?
  • is D'tor call on line 15 a good practice?

Please also point out of you see anything which I can improve on or just do not try. Any good reference or reads are also welcome.

like image 805
vikrant Avatar asked Jan 29 '16 14:01

vikrant


2 Answers

It's really, really simple: new can be thought of as doing two things:

  1. Allocating the memory.
  2. Placement-constructing the object in the allocated memory.

There's no guarantee that malloc is actually used by the implementation, but typically it is. You cannot assume it about the implementation, but for the purpose of understanding it's an OK assumption.

Thus, the following are thought of as being equivalent:

auto obj1 = new std::string("1");
// ↑ can be thought of as equivalent to ↓ 
auto obj2 = (std::string*)malloc(sizeof(std::string));
new(obj2) std::string("2");

Same goes for delete:

delete obj1;
// ↑ can be thought of as equivalent to ↓ 
obj2->~string();
free(obj2);

You can then easily reason about it all when you see new and delete for what they really are: an allocation followed by constructor call, and a destructor call followed by deallocation.

When you use placement new, you've decided to take care of the first step separately. The memory has to be still allocated somehow, you just get to have full control over how it happens and where does the memory come from.

You thus must keep track of two things, separately:

  1. The lifetime of the memory.

  2. The lifetime of the object.

The code below demonstrates how these are independent of each other:

#include <cstdlib>
#include <string>
#include <new>

using std::string;

int main() {
    auto obj = (string*)malloc(sizeof(string));  // memory is allocated
    new(obj) string("1");  // string("1") is constructed
    obj->~string ();       // string("1") is destructed
    new(obj) string("2");  // string("2") is constructed
    obj->~string ();       // string("2") is destructed
    free(obj);             // memory is deallocated
}

Your program has UB if the lifetime of the object extends past the lifetime of memory. Make sure that the memory always outlives the life of the object. For example, this has UB:

void ub() {
    alignas(string) char buf[sizeof(string)]; // memory is allocated
    new(buf) string("1");                     // string("1") is constructed
} // memory is deallocated but string("1") outlives the memory!

But this is OK:

void ub() {
    alignas(string) char buf[sizeof(string)]; // memory is allocated
    new(buf) string("1");                     // string("1") is constructed
    buf->~string();                           // string("1") is destructed
}                                             // memory is deallocated

Note how you need to properly align the automatic buffer using alignas. The lack of alignas for an arbitrary type results in UB. It might appear to work, but that's only to mislead you.

There are some specific types where not calling the destructor and not aligning the memory properly does not lead to UB, but you should never assume such things about a type. Call your destructors and do the alignment, it won't cost you anything if it turns out to be unnecessary - no extra code would be generated for such a type.

struct S {
  char str[10];
}
like image 136
Kuba hasn't forgotten Monica Avatar answered Oct 06 '22 20:10

Kuba hasn't forgotten Monica


This is probably something for CodeReview.SE, let me comment your sourcecode a bit before I answer your questions.

A *obj1 = new A(21);
std::cout<<"Object allocated at "<<obj1<<std::endl;
obj1->~A();

You'd normally never call the destructor on a object not created with placement-new. In your case, you destruct the old one and construct a new one with placement-new. Even though this works, you should rather implement some reset functionality to reset your object instead of destructing and constructing a new one.

17    obj1->testFunction();

This is UB. You already destructed the object, you shouldn't call any methods on it.

18    A *obj2 = new(obj1) A(22);
19    obj1->testFunction();
20    obj2->testFunction();

This is okayish, note though that obj1 and obj2 is the exact same object.

21    delete(obj1);// Is it really needed now? Here it will delete both objects.. so this is not the right place.

Your comment is wrong. You're not deleting two objects, you're deleting one, more later.

22    obj1->testFunction();
23    obj2->testFunction();

This is - again - UB, don't call methods on a deconstructed or deleted object. To your questions:

member _a is dynamically allocated (with no placement new). So why it is getting same same address for obj1 & obj2. Is it just a coincidence?

Don't call them obj1 and obj2 because those two variables point to the same object, but yes, it is coincidence. After the first object got destructed and free'd this memory, the second one allocated the same amount of memory which was just freed and the allocator decided to give you the exact same memory.

is D'tor call on line 15 a good practice?

No, its not. There are very few examples why you'd need to call the destructor, one of them is that your object was created by placement-new. In your example this has no sideeffects because you construct a new object in the same place after deconstructing the old one and the new object is of the same type as the old one, otherwise this could break badly in some way.

Now a bit more about your comment after delete. Lets see what a new and a placement-new actually does.

A new does:

  • Allocate memory from the OS for the new object
  • Call the constructor on the new object, the address (this) is set to the block of memory the allocator got.

The delete does the opposite:

  • Call the destructor of the object
  • Deallocate the chunk of memory

Now to the placement-new: The placement-new just skips the first step (allocating memory) and calls the constructor of that new object with this set to the address you passed. The opposite of placement-new is therefore just calling the destructor as there does not exist a placement-delete.

This means for your code, after you called the destructor, your first object died but you never gave the memory back, thats why you can construct a new object in that memory. Now when you call delete, the first object does not exist anymore, only the memory it used, but that same memory is now obstructed by the second object, therefore when you call delete you don't delete two objects, you delete only the second one (you deconstruct it and then deallocate the chunk of memory).

You can read more about the topic placement-new and when to call the destructor at isocpp's faq

like image 24
tkausl Avatar answered Oct 06 '22 18:10

tkausl