Do we really need placement new-expressions?

180 Views Asked by At

I am trying to understand placement new-expressions in C++.

This Stack Overflow answer states that T* p = new T(arg); is equivalent to

void* place = operator new(sizeof(T));  // storage allocation
T* p = new(place) T(arg);               // object construction

and that delete p; is equivalent to

p->~T();             // object destruction
operator delete(p);  // storage deallocation

Why do we need the placement new-expression in T* p = new(place) T(arg); for object construction, isn’t the following equivalent?

T* p = (T*) place;
*p = T(arg);
3

There are 3 best solutions below

6
Evg On BEST ANSWER

The first thing to note is that *p = T(arg); is an assignment, not a construction.

Now let's read the standard ([basic.life]/1):

... The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • its initialization (if any) is complete (including vacuous initialization)

For a general type T, initialization could have been completed if placement new were used, but that's not the case. Doing

void* place = operator new(sizeof(T));
T* p = (T*)place;

doesn't start the lifetime of *p.

The same section reads ([basic.life]/6):

... Before the lifetime of an object has started but after the storage which the object will occupy has been allocated ... any pointer that represents the address of the storage location where the object will be ... located may be used but only in limited ways. ... 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, ...

operator= is a non-static member function and doing *p = T(arg);, which is equivalent to p->operator=(T(arg)), results in undefined behaviour.

A trivial example is a class that contains a pointer as a data member that is initialized in the constructor and is dereferenced in the assignment operator. Without placement new the constructor won't be called and that pointer won't be initialized (complete example).

0
Timo On

Placement new has its use cases. One example is small buffer optimization to avoid heap allocations:

struct BigObject
{
    std::size_t a, b, c;
};

int main()
{    
    std::byte buffer[24];

    BigObject* ptr = new(buffer) BigObject {1, 2, 3};

    // use ptr ...

    ptr->~BigObject();
}

This example will create a BigObject instance inside buffer, which itself is an object located on the stack. As you can see, we don't allocate any memory ourselves here, therefore we also don't deallocate it (we don't call delete here). However we still have to destroy the object by calling the destructor.

Placement new in your specific example makes not a lot of sense since you essentially do the work of the new operator yourself. But as soon as you split up memory allocation and object construction, you need placement new.


As for your

T* p = (T*) place;
*p = T(arg);

example: as Evg already mentioned in the comments, you're dereferencing a pointer to uninitialized memory. p doesn't point to a T object yet, therefore dereferencing it is UB.

1
Serge Ballesta On

An example use case is a union containing a non-trivial type. You will have to explicitly construct the non-trivial member and explicitly destroy it:

#include <iostream>

struct Var {
    enum class Type { INT = 0, STRING } type;
    union { int val; std::string name; };
    Var(): type(Type::INT), val(0) {}
    ~Var() { if (type == Type::STRING) name.~basic_string(); }
    Var& operator=(int i) {
        if (type == Type::STRING) {
            name.~basic_string();  // explicit destruction required
            type = Type::INT;
        }
        val = i;
        return *this;
    }
    Var& operator=(const std::string& str) {
        if (type != Type::STRING) {
            new (&name) std::string(str);  // in-place construction
            type = Type::STRING;
        } else
            name = str;
        return *this;
    }
};

int main() {
    Var var;      // var is default initialized with a 0 int
    var = 12;     // val assignment
    std::cout << var.val << "\n";
    var = "foo";  // name assignment
    std::cout << var.name << "\n";
    return 0;
}

Starting with C++17, we have the std::variant class that does that under the hood, but if you use a C++14 or earlier version, you have to do it by hand

BTW, a real world class should contain a stream injector and extractor, and should have getters able to raise an exception if you do not access the current value. They are omitted here for brevity…