Bug 121068 - Placement new of array element is rejected at compile-time
Summary: Placement new of array element is rejected at compile-time
Status: ASSIGNED
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: 16.0
: P3 normal
Target Milestone: ---
Assignee: Jason Merrill
URL:
Keywords: rejects-valid
Depends on:
Blocks: constexpr
  Show dependency treegraph
 
Reported: 2025-07-14 14:31 UTC by Tomasz Kamiński
Modified: 2025-08-21 17:53 UTC (History)
4 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2025-07-21 00:00:00


Attachments
fix (2.42 KB, patch)
2025-07-16 20:22 UTC, Jason Merrill
Details | Diff
revised clobber patch (5.47 KB, patch)
2025-07-22 21:42 UTC, Jason Merrill
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Tomasz Kamiński 2025-07-14 14:31:18 UTC
For the following code:
```
consteval int
foo()
{
    using T = int;
    union { T arr[3]; };
    new(arr) T[3];
    for (int i = 0; i < 3; ++i)
      arr[i].~T();

    new (arr + 2) T{10}; // A

    return 1;
};

constexpr int g = foo();
```
https://godbolt.org/z/4bdPbqqvz

Placement new at line A is rejected with following error:
<source>:19:22:   in 'constexpr' expansion of 'foo()'
<source>:14:23: error: accessing uninitialized member 'foo()::<unnamed union>::arr'
   14 |     new (arr + 2) T{10};
      |                       ^
<source>:9:15: note: initializing 'foo()::<unnamed union>::arr' requires a member access expression as the left operand of the assignment
    9 |     union { T arr[3]; };
      |               ^~~
Compiler returned: 1
Comment 1 Jason Merrill 2025-07-16 14:26:39 UTC
Ah, the actual bug is that we don't represent the first array new because it's trivial default initialization, and we don't represent the trivial pseudo-destructor calls, so we don't see any access to arr until we get to actually trying to give it a value.

The error is correct under the current standard, but I want to make it well-formed by making the first array new make arr the active member.
Comment 2 Tomasz Kamiński 2025-07-16 15:41:10 UTC
That is indeed very surprising, as it would mean that if I have:
```
struct S {
  union {
    int x;
  };
};

constexpr S test()
{
  S s;
  new(&s.x) int;
  is_active_member(s.x); // this is false
}
```
One we have ability to ask for active member, we can observe if new marked it active.

However, marking the member as active is also breaking, as following will no longer compiler:
```
constexpr S create()
{
   S s;
   new(&s.x) int;
   return s;
}

constexpr S g = create(); // OK now, x is not active,
                          // ILL-FORMED if new will start lifetime of s.x
```
But I think this should be ILL-FORMED.
Comment 3 Jason Merrill 2025-07-16 20:22:45 UTC
Created attachment 61891 [details]
fix

Let me know how this works for you.
Comment 4 Tomasz Kamiński 2025-07-17 12:55:14 UTC
Hi, the original example works, but when I start to add library fluff, I get the same error. I mean cases like:
// passing address to actual member
new(&arr) T[3];
new(std::addressof(arr)) T[3]; // Disable overload op&

// Disabling ADL
new(static_cast<void*>(arr)) T[3];
new(static_cast<void*>(&arr)) T[3];
new(static_cast<void*>(std::addressof(arr))) T[3];
Comment 5 Jason Merrill 2025-07-17 15:10:42 UTC
The intent of the patch was to support 
  new (&union_.member) T
syntax like
  union_.member = T()
for setting the active member, as in
https://eel.is/c++draft/class.union#general-example-3
but adding the library fluff obscures that syntax, and so is less clear that it ought to work.
Comment 6 Tomasz Kamiński 2025-07-17 15:45:18 UTC
I believed that `_member` being active after new should directly fall from the definition of active member in https://eel.is/c++draft/class.union#general-2:
> In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended ([basic.life]).

So if new operations, starts lifetime of member of the union (regardless of how) it becomes active member of the union, until it's lifetime is ended. I do not think that syntax how does it happen matters.

The unclear case, would be if creating element of array member, should start the union, i.e. something like:
new (&union_.member[2]) T;
Comment 7 Tomasz Kamiński 2025-07-17 15:50:19 UTC
Or in other words, I believe my example is equivalent to implementation of optional,
where we have:
union {
  T val;
};

And then call:
  new(static_cast<void*>(addressof(val))) T(...);
It is just version were T is array.
Comment 8 Jason Merrill 2025-07-21 15:02:23 UTC
(In reply to Tomasz Kamiński from comment #6)
> I believed that `_member` being active after new should directly fall from
> the definition of active member in
> https://eel.is/c++draft/class.union#general-2:
> > In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended ([basic.life]).
> 
> So if new operations, starts lifetime of member of the union (regardless of
> how) it becomes active member of the union, until it's lifetime is ended. I
> do not think that syntax how does it happen matters.

Sure, that matches this comment in cxx_eval_store_expression:
          /* An INIT_EXPR of the last member in an access chain is always OK,                                                        

so I tried changing the clobber to happen regardless of the syntax of the placement argument.  This mostly worked well, including fixing an xfail in one of the constexpr new tests.  But it also ran into trouble on the current libstdc++ construct_at, which when told to construct a _Tp where _Tp is an array type, instead tries to construct a _Tp[1], which seems undefined to me; there is no such multidimensional array at that location.

This is related to LWG issue 3436.  The proposed resolution there seems reasonable, so I'm trying this library change:

index 217a0416d42..1ba9740e90d 100644
--- a/libstdc++-v3/include/bits/stl_construct.h
+++ b/libstdc++-v3/include/bits/stl_construct.h
@@ -104,7 +104,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
          static_assert(sizeof...(_Args) == 0, "std::construct_at for array "
                       "types must not use any arguments to initialize the "
                       "array");
-         return ::new(__loc) _Tp[1]();
+         __loc = ::new(__loc) _Tp();
+#if __cpp_lib_launder
+         return std::launder(__location);
+#else
+         return static_cast<_Tp*>(__loc);
+#endif
Comment 9 Tomasz Kamiński 2025-07-22 07:25:10 UTC
I remember that LWG3436 was discussed in core in Varna (https://wiki.edg.com/bin/view/Wg21varna/CoreWorkingGroup#LWG3436) and the current wording is result from there. 

I was always suspicious about creating Tp[1] there, as in case when we are creating a member (or any other object that is not transparently replaceable), we will simply reuse storage and destroy the original object. I do not think there is UB caused by lack of multidimensional array at that location, but the new call is for sure not constant for the same reason.

I agree that your proposed change seem to be much better direction, but as far as I can see it does not match direction of https://cplusplus.github.io/LWG/issue3436. Am I looking at wrong place?
Comment 10 Tomasz Kamiński 2025-07-22 14:00:32 UTC
If the object pointed by __location is not transparently replaceable, for example if I would create an object inside std::byte array that is an member, then does std::launder(__location) produce pointer to new object? __location does not automatically point there per (https://eel.is/c++draft/basic.life#10), by we meet precondition for launder (https://eel.is/c++draft/ptr.launder#2).

This cannot happen for compile-time evaluation, as calling construct_at such way would require equivalent of reinterpret_cast, so we could do, but I am not sure if that is necessary.
+#if __cpp_lib_launder
+  if consteval 
+    {
+      return std::launder(__location);
+    }
+  else
+    {
+      return std::launder(static_cast<_Tp*>(__loc));
+    }
+#else
+  return static_cast<_Tp*>(__loc);
+#endif
Comment 11 Jason Merrill 2025-07-22 17:20:44 UTC
(In reply to Tomasz Kamiński from comment #9)
> I remember that LWG3436 was discussed in core in Varna
> (https://wiki.edg.com/bin/view/Wg21varna/CoreWorkingGroup#LWG3436) and the
> current wording is result from there. 

Ah, right.

> I was always suspicious about creating Tp[1] there, as in case when we are
> creating a member (or any other object that is not transparently
> replaceable), we will simply reuse storage and destroy the original object.
> I do not think there is UB caused by lack of multidimensional array at that
> location, but the new call is for sure not constant for the same reason.

I guess the Tp[1] idea was justified by https://eel.is/c++draft/basic#compound-3 "an object of type T that is not an array element is considered to belong to an array with one element of type T" but that's qualified by "For purposes of pointer arithmetic and comparison", I don't see that it applies to the whole object model.

My argument that this is UB is based on https://eel.is/c++draft/basic.lval#11 , thinking that int[3] is not accessible through int[3][1].  But that section also makes the point that access is based on scalars, so perhaps it doesn't apply.

Actually, it's unclear to me what actually prevents us from creating a struct of one int overlaying an int....

> I agree that your proposed change seem to be much better direction, but as
> far as I can see it does not match direction of
> https://cplusplus.github.io/LWG/issue3436. Am I looking at wrong place?

Ah, no, I was; https://www.open-std.org/jtc1/sc22/wg21/docs/lwg-active.html seems to be out of date.

I sent mail to the core reflector.
Comment 12 GCC Commits 2025-07-22 21:37:16 UTC
The trunk branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:fdbc5ff61b471076cc9c758fb6c30d62f7ef1c56

commit r16-2432-gfdbc5ff61b471076cc9c758fb6c30d62f7ef1c56
Author: Jason Merrill <jason@redhat.com>
Date:   Wed Jul 16 11:52:45 2025 -0400

    c++: constexpr union placement new [PR121068]
    
    The note and example in [class.union] p6 think that placement new can be
    used to change the active member of a union, but we didn't support that for
    array members in constant-evaluation even after implementing P1330 and
    P2747.
    
    First I tried to address this by introducing a CLOBBER_BEGIN_OBJECT for the
    entire array, but that broke the resolution of LWG3436, which invokes 'new
    T[1]' for an array T, and trying to clobber a multidimensional array when
    the actual object is single-dimensional breaks.  So I've raised that issue
    with the committee.  Until that is resolved, this patch takes a simpler
    approach: allow initialization of an element of an array to make the array
    the active member of a union.
    
            PR c++/121068
    
    gcc/cp/ChangeLog:
    
            * constexpr.cc (cxx_eval_store_expression): Allow ARRAY_REFs
            when activating an array member of a union.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp2a/constexpr-union6.C: Expect x5 to work.
            * g++.dg/cpp26/constexpr-new4.C: New test.
Comment 13 Jason Merrill 2025-07-22 21:42:18 UTC
Created attachment 61942 [details]
revised clobber patch

Here's the current state of the patch to clobber arrays to start their lifetime.
Comment 14 Tomasz Kamiński 2025-07-23 08:30:15 UTC
Not sure if this is expected, so noting this here.

On trunk (with r16-2432-gfdbc5ff61b471076cc9c758fb6c30d62f7ef1c56), if I value initialize the array (like in inplace_vector), then the code works:
  union { S a[20]; };
  new (&a) S[20](); // OK
  for (int i = 0; i < 20; ++i)
    a[i].~S();

  auto* sf = ::new(&a[2]) S(11);
See also: https://gcc.gnu.org/pipermail/libstdc++/2025-July/062752.html

But after changing the placement new to perform default initialization `new (&a) S`, the above code no longer works. This is not a blocker on any library feature.
Comment 15 Jason Merrill 2025-07-25 14:12:44 UTC
(In reply to Tomasz Kamiński from comment #14)
Please provide complete testcases, this snippet isn't enough to reproduce.
Comment 16 Tomasz Kamiński 2025-07-25 14:18:48 UTC
Ah sorry, I was sure I posted the function before:
```
#include <new>

struct S
{
  constexpr S() = default;
  constexpr S(int x) : s(x) {}
  constexpr S(S&& x) : s(x.s) {}                                                                                                                               
  constexpr S& operator=(S&& x) { s = x.s; return *this; }                                                                                                     
  unsigned char s;                                                                                                                                             
};

constexpr
int foo()
{
  union { S a[20]; };
  new (&a) S[20](); // OK
  for (int i = 0; i < 20; ++i)
    a[i].~S();

  auto* sf = ::new(&a[2]) S(11);
  return 1;
}

static_assert(foo());

constexpr
int foo2()
{
  union { S a[20]; };
  new (&a) S[20]; // ILL-FORMED
  for (int i = 0; i < 20; ++i)
    a[i].~S();

  auto* sf = ::new(&a[2]) S(11);
  return 1;
}

static_assert(foo2());
```
https://godbolt.org/z/cKbEsdzEh
Comment 17 Jason Merrill 2025-07-25 18:32:26 UTC
OK, the issue is that we currently don't represent trivial initialization at all, so the initial placement new has no effect.

Then the trivial destructors are represented by clobbers, but constant evaluation doesn't currently do anything with them (before my WIP patch).

Then the individual construction starts with a clobber that the evaluator ignores.

Then the constructor starts to initialize the individual member, but that's not directly initializing the union member, so it gets rejected.

As in the WIP patch I think the solution is to represent trivial initialization by clobbering the object instead of doing nothing.
Comment 18 GCC Commits 2025-08-05 22:19:48 UTC
The trunk branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:bc42128330c0ea70a015b74b655cb8c48b6a8c06

commit r16-3022-gbc42128330c0ea70a015b74b655cb8c48b6a8c06
Author: Jason Merrill <jason@redhat.com>
Date:   Tue Aug 5 15:16:50 2025 -0700

    c++: clobber object on placement new [PR121068]
    
    My r16-2432 patch addressed the original testcase involving an array of
    scalars, but not this additional testcase involving an array of classes.
    
    This patch addresses the issue more thoroughly, by having placement new
    first clobber the new object, and improving cxx_eval_store_expression to
    implement initial clobbers as well.
    
    My earlier attempt to do this clobbered the array as a whole, which broke
    construct_at after the resolution of LWG3436 due to trying to create a
    multidimensional array over the top of a single-dimensional array.  To
    side-step that issue, this patch instead clobbers the individual elements of
    an array, taking advantage of the earlier change to let that activate the
    array member of a union.
    
            PR c++/121068
    
    gcc/cp/ChangeLog:
    
            * constexpr.cc (cxx_eval_store_expression): Handle clobbers.
            (potential_constant_expression_1): Handle clobbers more.
            * decl.cc (build_clobber_this): Use INIT_EXPR for initial clobber.
            * init.cc (build_new_1): Clobber on placement new.
            (build_vec_init): Don't clean up after clobber.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp26/constexpr-new5.C: New test.
Comment 19 Jonathan Wakely 2025-08-21 08:56:26 UTC
I'm seeing a new libstdc++ testsuite failure since r16-3022-gbc42128330c0ea

FAIL: 20_util/variant/102912.cc  -std=gnu++20 (test for excess errors)
FAIL: 20_util/variant/102912.cc  -std=gnu++23 (test for excess errors)
FAIL: 20_util/variant/102912.cc  -std=gnu++26 (test for excess errors)

The error is:

in 'constexpr' expansion of 'std::destroy_at<const int>(__pointer)'
/home/jwakely/gcc/15/include/c++/15.1.1/bits/stl_construct.h:164:22:   
  164 |       std::destroy_at(__pointer);
      |       ~~~~~~~~~~~~~~~^~~~~~~~~~~
/home/jwakely/gcc/15/include/c++/15.1.1/bits/stl_construct.h:88:9: error: modifying a const object '* __location' is not allowed in a constant expression
   88 |         __location->~_Tp();
      |         ^~~~~~~~~~
/home/jwakely/gcc/15/include/c++/15.1.1/variant:246:13: note: originally declared 'const' here
  246 |       _Type _M_storage;
      |             ^~~~~~~~~~
Comment 20 Jason Merrill 2025-08-21 16:34:42 UTC
(In reply to Jonathan Wakely from comment #19)
> FAIL: 20_util/variant/102912.cc  -std=gnu++20 (test for excess errors)

Ah, thanks, I was only running that test in 17 mode (the default).  I'll fix that as well.
Comment 21 GCC Commits 2025-08-21 17:53:30 UTC
The trunk branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:70f33ad677e6350a724b56d4cb766480ed8367fc

commit r16-3333-g70f33ad677e6350a724b56d4cb766480ed8367fc
Author: Jason Merrill <jason@redhat.com>
Date:   Thu Aug 21 13:52:25 2025 -0400

    c++: constexpr clobber of const [PR121068]
    
    Since r16-3022, 20_util/variant/102912.cc was failing in C++20 and above due
    to wrong errors about destruction modifying a const object; destruction is
    OK.
    
            PR c++/121068
    
    gcc/cp/ChangeLog:
    
            * constexpr.cc (cxx_eval_store_expression): Allow clobber of a const
            object.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp2a/constexpr-dtor18.C: New test.