2

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);
Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
  • 9
    `*p = T(arg);` is an assignment, not construction. But you can't invoke an assignment on something that has never been constructed. – Evg Sep 13 '21 at 06:59
  • @Evg You are right, a temporary object is constructed and the move constructor is called on it. But what is the problem, can’t you move from a temporary object to an object with dynamic storage duration? – Géry Ogam Sep 13 '21 at 07:44
  • 2
    There is no object to move to! It has not been constructed. – Evg Sep 13 '21 at 07:47
  • There is no way to create an object at a predetermined memory location other than placement new. – StoryTeller - Unslander Monica Sep 13 '21 at 07:47
  • 5
    @Maggyero "and the move constructor is called on it." - No, if you write `*p = T(arg)`, the *move assignment operator* is called, not the *move constructor*. And calling the assignment operator on uninitialized memory is undefined behavior. – Sebastian Redl Sep 13 '21 at 07:50
  • @Evg Sorry I mixed up the move constructor with the move assignment operator. You are right, there is no object to move to. – Géry Ogam Sep 13 '21 at 07:52
  • @SebastianRedl I see. Could you write an answer with the quote from the C++ standard supporting this, from https://timsong-cpp.github.io/cppwp/? – Géry Ogam Sep 13 '21 at 07:56
  • Or @Evg since you were the first to comment? – Géry Ogam Sep 13 '21 at 07:58

3 Answers3

5

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

Evg
  • 25,259
  • 5
  • 41
  • 83
  • Perfect, that is exactly the information I needed. Thanks! – Géry Ogam Sep 13 '21 at 08:42
  • About your trivial example, could you provide the code because I don’t understand why the constructor won’t be called. Because in `*p = T(arg);` the constructor *is* called to create a temporary object which is then bound to the reference parameter of the move assignment operator (which is undefined behaviour as you explained). – Géry Ogam Sep 13 '21 at 09:07
  • @Maggyero https://godbolt.org/z/PhzPYajcj It is about the constructor of an object you're assigning *to*. – Evg Sep 13 '21 at 09:14
  • I see, the problem is that using a dereferenced null pointer is undefined behaviour (cf. [DR 1102](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102)). I have just edited your answer to make that clear. – Géry Ogam Sep 13 '21 at 10:08
  • 1
    @Maggyero It is not guaranteed to be null, it is uninitialized. Even accessing a pointer value itself is undefined behaviour. – Evg Sep 13 '21 at 10:21
  • My bad, [you are right](https://timsong-cpp.github.io/cppwp/basic#indet-1): ‘When storage for an object with automatic or dynamic storage duration is obtained, the object has an *indeterminate value,* and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced ([expr.ass]). If an indeterminate value is produced by an evaluation, the behavior is undefined except in the following cases:’ – Géry Ogam Sep 13 '21 at 10:33
1

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…

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Very instructive use case, thanks! That was the occasion to learn union classes. Shouldn’t the statement `name = str;` be in an `else` clause to avoid unnecessary assignment after construction? – Géry Ogam Sep 13 '21 at 11:50
0

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.

Timo
  • 9,269
  • 2
  • 28
  • 58