7
struct A
{
    int x;
}

A t{};
t.x = 5;

new (&t) A;

// is it always safe to assume that t.x is 5?
assert(t.x == 5);

As far as I know, when a trivial object of class type is created, the compiler can omit the call of explicit or implicit default constructor because no initialization is required. (is that right?)

Then, If placement new is performed on a trivial object whose lifetime has already begun, is it guaranteed to preserve its object/value representation? (If so, I want to know where I can find the specification..)

Eunho Choi
  • 167
  • 6
  • 2
    No, this is not safe. You never initialized the member when constructing, so you are reading an indeterminate value. – NathanOliver Mar 01 '22 at 14:25
  • 2
    In this pseudo-code, where the comment & assert are located, it is always safe to assume that t.x is uninitialized (*because it is uninitialized*) and accessing it before it has been initialized is **undefined behavior**. – Eljay Mar 01 '22 at 14:27
  • @Eljay is placement new regarded as default-initialization in the above code? that's why t.x after placement new is uninitialized while `A t{}` initializes t.x by performing zero-initializaiton? – Eunho Choi Mar 01 '22 at 14:41
  • 2
    Placement `new` ends the lifetime of the object that previously occupied the storage (if you didn't previously end it with a call to the destructor). It is a good habit to use the return value of the `new` expression instead of using reusing `t`. – François Andrieux Mar 01 '22 at 14:41
  • Imagine you have a DS9K computer. For that computer, every byte of memory has status bits: r, w, x, and e. For an uninitialized byte of memory, e-bit is set, r-bit is clear, w-bit is set. If read with the e-bit set, the read faults and the program abends. If written to, the e-bit is cleared, the r-bit is set, and then future reads succeed. Your C++ compiler's optimizer knows these things. Trying to dance around the compiler's understanding of the C++ abstract machine will only end in tears. – Eljay Mar 01 '22 at 14:49
  • [What Every C Programmer Should Know About Undefined Behavior](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html), also applies to C++. The answer from aschepler makes a good case that the code is actually safe. I would not rely on this in production code. – Eljay Mar 01 '22 at 14:58
  • 2
    [clang says it's uninitialized](https://godbolt.org/z/nb9xqs59r) (though this example might not be equivalent) – Artyer Mar 01 '22 at 15:21
  • 1
    Similar question: https://stackoverflow.com/q/14659752 – Artyer Mar 01 '22 at 15:31

2 Answers2

7

Well, let's ask some compilers for their opinion. Reading an indeterminate value is UB, which means that if it occurs inside a constant expression, it must be diagnosed. We can't directly use placement new in a constant expression, but we can use std::construct_at (which has a typed interface). I also modified the class A slightly so that value-initialization does the same thing as default-initialization:

#include <memory>

struct A
{
    int x;
    constexpr A() {}
};

constexpr int foo() {
    A t;
    t.x = 5;
    std::construct_at(&t);
    return t.x;
}

static_assert(foo() == 5);

As you can see on Godbolt, Clang, ICC, and MSVC all reject the code, saying that foo() is not a constant expression. Clang and MSVC additionally indicate that they have a problem with the read of t.x, which they consider to be a read of an uninitialized value.

P0593, while not directly related to this issue, contains an explanation that seems relevant:

The properties ascribed to objects and references throughout this document apply for a given object or reference only during its lifetime.

That is, reusing the storage occupied by an object in order to create a new object always destroys whatever value was held by the old object, because an object's value dies with its lifetime. Now, objects of type A are transparently replaceable by other objects of type A, so it is permitted to continue to use the name t even after its storage has been reused. That does not imply that the new t holds the value that the old t does. It only means that t is not a dangling reference to the old object.

Going off what is said in P0593, GCC is wrong and the other compilers are right. In constant expression evaluation, this kind of code is required to be diagnosed. Otherwise, it's just UB.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • Correct me if I'm wrong, but isn't `constexpr A() {}` ill-formed, NDR? I was under the impression that a constructor cannot be a constant expression unless it initializes all data members -- which would make the `std::construct_at(...)` test inconclusive. I don't disagree with the conclusion at all; but I think using a potentially bad constant expression doesn't prove it. Not sure if C++20 changed this rule to be valid for some reason – Human-Compiler Mar 02 '22 at 18:54
  • 1
    @Human-Compiler The restriction you are talking about was lifted in C++20. Prior to C++20, it was IF (not IFNDR). – Brian Bi Mar 02 '22 at 19:16
2

From looking at the Standard, the program has undefined behavior because of an invalid use of an object with indeterminate value.

Per [basic.life]/8, since the object of type A created by the placement new-expression exactly overlays the original object t, using the name t after that point refers to the A object created by the new-expression.

In [basic.indet]/1, we have:

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

One important detail here (which I missed at first) is that "obtaining storage" is different from "allocating storage" or the storage duration of a storage region. The "obtain storage" words are also used to define the beginning of an object's lifetime in [basic.life]/1 and in the context of a new-expression in [expr.new]/10:

A new-expression may obtain storage for the object by calling an allocation function ([basic.stc.dynamic.allocation]). ... [ Note: ... The set of allocation and deallocation functions that may be called by a new-expression may include functions that do not perform allocation or deallocation; for example, see [new.delete.placement]. — end note ]

So the placement new-expression "obtains storage" for the object of type A and its subobject of type int when it calls operator new(void*). For this purpose, it doesn't make a difference that the memory locations in the storage region actually have static storage duration. Since "no initialization is performed" for the created subobject of type int with dynamic storage duration, it has an indeterminate value.

See also this Q&A: What does it mean to obtain storage?

aschepler
  • 70,891
  • 9
  • 107
  • 161