[pushed,4/4] c++: End lifetime of objects in constexpr after destructor call [PR71093]

Message ID 20231213164740.1591535-4-jason@redhat.com
State Committed
Commit 90bc2d09b5bfcc913f79543c3b65202e7246e162
Headers
Series [pushed,1/4] c++: copy location to AGGR_INIT_EXPR |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gcc_build--master-arm warning Patch is already merged
linaro-tcwg-bot/tcwg_gcc_build--master-aarch64 warning Patch is already merged

Commit Message

Jason Merrill Dec. 13, 2023, 4:47 p.m. UTC
  Tested x86_64-pc-linux-gnu, applying to trunk.

This is modified from Nathaniel's last version by adjusting for my recent
CLOBBER changes and removing the special handling of __in_chrg which is no
longer needed since my previous commit.

-- 8< --

This patch adds checks for using objects after they've been manually
destroyed via explicit destructor call. Currently this is only
implemented for 'top-level' objects; FIELD_DECLs and individual elements
of arrays will need a lot more work to track correctly and are left for
a future patch.

The other limitation is that destruction of parameter objects is checked
too 'early', happening at the end of the function call rather than the
end of the owning full-expression as they should be for consistency;
see cpp2a/constexpr-lifetime2.C. This is because I wasn't able to find a
good way to link the constructed parameter declarations with the
variable declarations that are actually destroyed later on to propagate
their lifetime status, so I'm leaving this for a later patch.

	PR c++/71093

gcc/cp/ChangeLog:

	* constexpr.cc (constexpr_global_ctx::get_value_ptr): Don't
	return NULL_TREE for objects we're initializing.
	(constexpr_global_ctx::destroy_value): Rename from remove_value.
	Only mark real variables as outside lifetime.
	(constexpr_global_ctx::clear_value): New function.
	(destroy_value_checked): New function.
	(cxx_eval_call_expression): Defer complaining about non-constant
	arg0 for operator delete. Use remove_value_safe.
	(cxx_fold_indirect_ref_1): Handle conversion to 'as base' type.
	(outside_lifetime_error): Include name of object we're
	accessing.
	(cxx_eval_store_expression): Handle clobbers. Improve error
	messages.
	(cxx_eval_constant_expression): Use remove_value_safe. Clear
	bind variables before entering body.

gcc/testsuite/ChangeLog:

	* g++.dg/cpp1y/constexpr-lifetime1.C: Improve error message.
	* g++.dg/cpp1y/constexpr-lifetime2.C: Likewise.
	* g++.dg/cpp1y/constexpr-lifetime3.C: Likewise.
	* g++.dg/cpp1y/constexpr-lifetime4.C: Likewise.
	* g++.dg/cpp2a/bitfield2.C: Likewise.
	* g++.dg/cpp2a/constexpr-new3.C: Likewise. New check.
	* g++.dg/cpp1y/constexpr-lifetime7.C: New test.
	* g++.dg/cpp2a/constexpr-lifetime1.C: New test.
	* g++.dg/cpp2a/constexpr-lifetime2.C: New test.

Signed-off-by: Nathaniel Shead <nathanieloshead@gmail.com>
---
 gcc/cp/constexpr.cc                           | 148 +++++++++++++++---
 .../g++.dg/cpp1y/constexpr-lifetime1.C        |   2 +-
 .../g++.dg/cpp1y/constexpr-lifetime2.C        |   2 +-
 .../g++.dg/cpp1y/constexpr-lifetime3.C        |   2 +-
 .../g++.dg/cpp1y/constexpr-lifetime4.C        |   2 +-
 .../g++.dg/cpp1y/constexpr-lifetime7.C        |  93 +++++++++++
 gcc/testsuite/g++.dg/cpp2a/bitfield2.C        |   2 +-
 .../g++.dg/cpp2a/constexpr-lifetime1.C        |  21 +++
 .../g++.dg/cpp2a/constexpr-lifetime2.C        |  23 +++
 gcc/testsuite/g++.dg/cpp2a/constexpr-new3.C   |  17 +-
 10 files changed, 284 insertions(+), 28 deletions(-)
 create mode 100644 gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime7.C
 create mode 100644 gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime1.C
 create mode 100644 gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime2.C
  

Patch

diff --git a/gcc/cp/constexpr.cc b/gcc/cp/constexpr.cc
index 9d9e96c2afd..e1b2d27fc36 100644
--- a/gcc/cp/constexpr.cc
+++ b/gcc/cp/constexpr.cc
@@ -1193,13 +1193,20 @@  public:
 	return *p;
     return NULL_TREE;
   }
-  tree *get_value_ptr (tree t)
+  tree *get_value_ptr (tree t, bool initializing)
   {
     if (modifiable && !modifiable->contains (t))
       return nullptr;
     if (tree *p = values.get (t))
-      if (*p != void_node)
-	return p;
+      {
+	if (*p != void_node)
+	  return p;
+	else if (initializing)
+	  {
+	    *p = NULL_TREE;
+	    return p;
+	  }
+      }
     return nullptr;
   }
   void put_value (tree t, tree v)
@@ -1208,13 +1215,19 @@  public:
     if (!already_in_map && modifiable)
       modifiable->add (t);
   }
-  void remove_value (tree t)
+  void destroy_value (tree t)
   {
-    if (DECL_P (t))
+    if (TREE_CODE (t) == VAR_DECL
+	|| TREE_CODE (t) == PARM_DECL
+	|| TREE_CODE (t) == RESULT_DECL)
       values.put (t, void_node);
     else
       values.remove (t);
   }
+  void clear_value (tree t)
+  {
+    values.remove (t);
+  }
 };
 
 /* Helper class for constexpr_global_ctx.  In some cases we want to avoid
@@ -1238,7 +1251,7 @@  public:
   ~modifiable_tracker ()
   {
     for (tree t: set)
-      global->remove_value (t);
+      global->clear_value (t);
     global->modifiable = nullptr;
   }
 };
@@ -1278,6 +1291,40 @@  struct constexpr_ctx {
   mce_value manifestly_const_eval;
 };
 
+/* Remove T from the global values map, checking for attempts to destroy
+   a value that has already finished its lifetime.  */
+
+static void
+destroy_value_checked (const constexpr_ctx* ctx, tree t, bool *non_constant_p)
+{
+  if (t == error_mark_node || TREE_TYPE (t) == error_mark_node)
+    return;
+
+  /* Don't error again here if we've already reported a problem.  */
+  if (!*non_constant_p
+      && DECL_P (t)
+      /* Non-trivial destructors have their lifetimes ended explicitly
+	 with a clobber, so don't worry about it here.  */
+      && (!TYPE_HAS_NONTRIVIAL_DESTRUCTOR (TREE_TYPE (t))
+	  /* ...except parameters are remapped in cxx_eval_call_expression,
+	     and the destructor call during cleanup won't be able to tell that
+	     this value has already been destroyed, so complain now.  This is
+	     not quite unobservable, but is extremely unlikely to crop up in
+	     practice; see g++.dg/cpp2a/constexpr-lifetime2.C.  */
+	  || TREE_CODE (t) == PARM_DECL)
+      && ctx->global->is_outside_lifetime (t))
+    {
+      if (!ctx->quiet)
+	{
+	  auto_diagnostic_group d;
+	  error ("destroying %qE outside its lifetime", t);
+	  inform (DECL_SOURCE_LOCATION (t), "declared here");
+	}
+      *non_constant_p = true;
+    }
+  ctx->global->destroy_value (t);
+}
+
 /* This internal flag controls whether we should avoid doing anything during
    constexpr evaluation that would cause extra DECL_UID generation, such as
    template instantiation and function body copying.  */
@@ -2806,6 +2853,7 @@  cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
 	  && (CALL_FROM_NEW_OR_DELETE_P (t)
 	      || is_std_allocator_allocate (ctx->call)))
 	{
+	  const bool new_op_p = IDENTIFIER_NEW_OP_P (DECL_NAME (fun));
 	  const int nargs = call_expr_nargs (t);
 	  tree arg0 = NULL_TREE;
 	  for (int i = 0; i < nargs; ++i)
@@ -2813,12 +2861,15 @@  cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
 	      tree arg = CALL_EXPR_ARG (t, i);
 	      arg = cxx_eval_constant_expression (ctx, arg, vc_prvalue,
 						  non_constant_p, overflow_p);
-	      VERIFY_CONSTANT (arg);
+	      /* Deleting a non-constant pointer has a better error message
+		 below.  */
+	      if (new_op_p || i != 0)
+		VERIFY_CONSTANT (arg);
 	      if (i == 0)
 		arg0 = arg;
 	    }
 	  gcc_assert (arg0);
-	  if (IDENTIFIER_NEW_OP_P (DECL_NAME (fun)))
+	  if (new_op_p)
 	    {
 	      tree type = build_array_type_nelts (char_type_node,
 						  tree_to_uhwi (arg0));
@@ -2867,7 +2918,7 @@  cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
 			  return t;
 			}
 		      DECL_NAME (var) = heap_deleted_identifier;
-		      ctx->global->remove_value (var);
+		      ctx->global->destroy_value (var);
 		      ctx->global->heap_dealloc_count++;
 		      return void_node;
 		    }
@@ -2890,7 +2941,7 @@  cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
 			  return t;
 			}
 		      DECL_NAME (var) = heap_deleted_identifier;
-		      ctx->global->remove_value (var);
+		      ctx->global->destroy_value (var);
 		      ctx->global->heap_dealloc_count++;
 		      return void_node;
 		    }
@@ -3255,9 +3306,9 @@  cxx_eval_call_expression (const constexpr_ctx *ctx, tree t,
 				      non_constant_p, overflow_p);
 
 	  /* Remove the parms/result from the values map.  */
-	  ctx->global->remove_value (res);
+	  destroy_value_checked (ctx, res, non_constant_p);
 	  for (tree parm = parms; parm; parm = TREE_CHAIN (parm))
-	    ctx->global->remove_value (parm);
+	    destroy_value_checked (ctx, parm, non_constant_p);
 
 	  /* Free any parameter CONSTRUCTORs we aren't returning directly.  */
 	  while (!ctors->is_empty ())
@@ -5657,6 +5708,10 @@  cxx_fold_indirect_ref_1 (const constexpr_ctx *ctx, location_t loc, tree type,
 	      }
 	  }
 
+      /* Handle conversion to "as base" type.  */
+      if (CLASSTYPE_AS_BASE (optype) == type)
+	return op;
+
       /* Handle conversion to an empty base class, which is represented with a
 	 NOP_EXPR.  Do this before spelunking into the non-empty subobjects,
 	 which is likely to be a waste of time (109678).  */
@@ -5908,7 +5963,7 @@  outside_lifetime_error (location_t loc, tree r)
     }
   else
     {
-      error_at (loc, "accessing object outside its lifetime");
+      error_at (loc, "accessing %qE outside its lifetime", r);
       inform (DECL_SOURCE_LOCATION (r), "declared here");
     }
 }
@@ -6125,8 +6180,10 @@  cxx_eval_store_expression (const constexpr_ctx *ctx, tree t,
   constexpr_ctx new_ctx = *ctx;
 
   tree init = TREE_OPERAND (t, 1);
-  if (TREE_CLOBBER_P (init))
-    /* Just ignore clobbers.  */
+
+  if (TREE_CLOBBER_P (init)
+      && CLOBBER_KIND (init) < CLOBBER_OBJECT_END)
+    /* Only handle clobbers ending the lifetime of objects.  */
     return void_node;
 
   /* First we figure out where we're storing to.  */
@@ -6136,7 +6193,7 @@  cxx_eval_store_expression (const constexpr_ctx *ctx, tree t,
 
   tree type = TREE_TYPE (target);
   bool preeval = SCALAR_TYPE_P (type) || TREE_CODE (t) == MODIFY_EXPR;
-  if (preeval)
+  if (preeval && !TREE_CLOBBER_P (init))
     {
       /* Evaluate the value to be stored without knowing what object it will be
 	 stored in, so that any side-effects happen first.  */
@@ -6244,11 +6301,18 @@  cxx_eval_store_expression (const constexpr_ctx *ctx, tree t,
       && const_object_being_modified == NULL_TREE)
     const_object_being_modified = object;
 
+  if (DECL_P (object)
+      && TREE_CLOBBER_P (init)
+      && DECL_NAME (object) == heap_deleted_identifier)
+    /* Ignore clobbers of deleted allocations for now; we'll get a better error
+       message later when operator delete is called.  */
+    return void_node;
+
   /* And then find/build up our initializer for the path to the subobject
      we're initializing.  */
   tree *valp;
   if (DECL_P (object))
-    valp = ctx->global->get_value_ptr (object);
+    valp = ctx->global->get_value_ptr (object, TREE_CODE (t) == INIT_EXPR);
   else
     valp = NULL;
   if (!valp)
@@ -6256,10 +6320,45 @@  cxx_eval_store_expression (const constexpr_ctx *ctx, tree t,
       /* A constant-expression cannot modify objects from outside the
 	 constant-expression.  */
       if (!ctx->quiet)
-	error ("modification of %qE is not a constant expression", object);
+	{
+	  auto_diagnostic_group d;
+	  if (DECL_P (object) && DECL_NAME (object) == heap_deleted_identifier)
+	    {
+	      error ("modification of allocated storage after deallocation "
+		     "is not a constant expression");
+	      inform (DECL_SOURCE_LOCATION (object), "allocated here");
+	    }
+	  else if (DECL_P (object) && ctx->global->is_outside_lifetime (object))
+	    {
+	      if (TREE_CLOBBER_P (init))
+		error ("destroying %qE outside its lifetime", object);
+	      else
+		error ("modification of %qE outside its lifetime "
+		       "is not a constant expression", object);
+	      inform (DECL_SOURCE_LOCATION (object), "declared here");
+	    }
+	  else
+	    {
+	      if (TREE_CLOBBER_P (init))
+		error ("destroying %qE from outside current evaluation "
+		       "is not a constant expression", object);
+	      else
+		error ("modification of %qE from outside current evaluation "
+		       "is not a constant expression", object);
+	    }
+	}
       *non_constant_p = true;
       return t;
     }
+
+  /* Handle explicit end-of-lifetime.  */
+  if (TREE_CLOBBER_P (init))
+    {
+      if (refs->is_empty ())
+	ctx->global->destroy_value (object);
+      return void_node;
+    }
+
   type = TREE_TYPE (object);
   bool no_zero_init = true;
 
@@ -6533,7 +6632,7 @@  cxx_eval_store_expression (const constexpr_ctx *ctx, tree t,
       /* The hash table might have moved since the get earlier, and the
 	 initializer might have mutated the underlying CONSTRUCTORs, so we must
 	 recompute VALP. */
-      valp = ctx->global->get_value_ptr (object);
+      valp = ctx->global->get_value_ptr (object, TREE_CODE (t) == INIT_EXPR);
       for (unsigned i = 0; i < vec_safe_length (indexes); i++)
 	{
 	  ctors[i] = valp;
@@ -7650,7 +7749,7 @@  cxx_eval_constant_expression (const constexpr_ctx *ctx, tree t,
 	/* Forget SAVE_EXPRs and TARGET_EXPRs created by this
 	   full-expression.  */
 	for (tree save_expr : save_exprs)
-	  ctx->global->remove_value (save_expr);
+	  destroy_value_checked (ctx, save_expr, non_constant_p);
       }
       break;
 
@@ -8203,13 +8302,18 @@  cxx_eval_constant_expression (const constexpr_ctx *ctx, tree t,
 				      non_constant_p, overflow_p, jump_target);
 
     case BIND_EXPR:
+      /* Pre-emptively clear the vars declared by this BIND_EXPR from the value
+	 map, so that when checking whether they're already destroyed later we
+	 don't get confused by remnants of previous calls.  */
+      for (tree decl = BIND_EXPR_VARS (t); decl; decl = DECL_CHAIN (decl))
+	ctx->global->clear_value (decl);
       r = cxx_eval_constant_expression (ctx, BIND_EXPR_BODY (t),
 					lval,
 					non_constant_p, overflow_p,
 					jump_target);
       for (tree decl = BIND_EXPR_VARS (t); decl; decl = DECL_CHAIN (decl))
-	ctx->global->remove_value (decl);
-      return r;
+	destroy_value_checked (ctx, decl, non_constant_p);
+      break;
 
     case PREINCREMENT_EXPR:
     case POSTINCREMENT_EXPR:
diff --git a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime1.C b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime1.C
index 43aa7c974c1..3fda29e0cc2 100644
--- a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime1.C
+++ b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime1.C
@@ -10,4 +10,4 @@  constexpr const int& test() {
   auto local = S{};  // { dg-message "note: declared here" }
   return local.get();
 }
-constexpr int x = test();  // { dg-error "accessing object outside its lifetime" }
+constexpr int x = test();  // { dg-error "accessing .local. outside its lifetime" }
diff --git a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime2.C b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime2.C
index 2f5ae8db6d5..d82ba5c8b73 100644
--- a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime2.C
+++ b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime2.C
@@ -8,7 +8,7 @@  struct S {
 
 constexpr int error() {
   const auto& local = S{}.get();  // { dg-message "note: declared here" }
-  return local;  // { dg-error "accessing object outside its lifetime" }
+  return local;  // { dg-error "accessing '\[^'\]+' outside its lifetime" }
 }
 constexpr int x = error();  // { dg-message "in .constexpr. expansion" }
 
diff --git a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime3.C b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime3.C
index 53785521d05..67e9b91c723 100644
--- a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime3.C
+++ b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime3.C
@@ -7,7 +7,7 @@  constexpr int f(int i) {
     int j = 123;  // { dg-message "note: declared here" }
     p = &j;
   }
-  return *p;  // { dg-error "accessing object outside its lifetime" }
+  return *p;  // { dg-error "accessing 'j' outside its lifetime" }
 }
 
 constexpr int i = f(0);  // { dg-message "in .constexpr. expansion" }
diff --git a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime4.C b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime4.C
index 181a1201663..6f0d749dcf2 100644
--- a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime4.C
+++ b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime4.C
@@ -5,7 +5,7 @@  constexpr const double& test() {
   return local;
 }
 
-static_assert(test() == 3.0, "");  // { dg-error "constant|accessing object outside its lifetime" }
+static_assert(test() == 3.0, "");  // { dg-error "constant|accessing '\[^'\]+' outside its lifetime" }
 
 // no deference, shouldn't error
 static_assert((test(), true), "");
diff --git a/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime7.C b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime7.C
new file mode 100644
index 00000000000..4148f42f7be
--- /dev/null
+++ b/gcc/testsuite/g++.dg/cpp1y/constexpr-lifetime7.C
@@ -0,0 +1,93 @@ 
+// PR c++/71093
+// { dg-do compile { target c++14 } }
+
+constexpr int f (const int *p)
+{
+  typedef int T;
+  p->~T ();   // { dg-error "destroying" }
+  return *p;
+}
+
+constexpr int i = 0;
+constexpr int j = f (&i);
+
+
+template <typename T>
+constexpr bool test_access() {
+  T x {};
+  x.~T();
+  T y = x;  // { dg-error "lifetime" }
+  return true;
+}
+
+template <typename T>
+constexpr bool test_modification() {
+  T x {};
+  x.~T();
+  x = T();  // { dg-error "lifetime" }
+  return true;
+}
+
+template <typename T>
+constexpr bool test_scope() {
+  {
+    T x {};
+    x.~T();
+  }  // { dg-error "destroying" }
+  return true;
+}
+
+template <typename T>
+constexpr bool test_destroy_temp() {
+  T{}.~T();  // { dg-error "destroying" }
+  return true;
+}
+
+template <typename T>
+constexpr bool test_parameter(T t) {
+  // note: error message occurs at point of call
+  t.~T();
+  return true;
+}
+
+template <typename T>
+constexpr void test_bindings_impl(int n) {
+  if (n == 0) return;
+  T a {};
+  if (n == 1) return;
+  T b {};
+}
+
+template <typename T>
+constexpr bool test_bindings() {
+  test_bindings_impl<T>(1);
+  test_bindings_impl<T>(0);
+  test_bindings_impl<T>(2);
+  return true;
+}
+
+constexpr bool i1 = test_access<int>();        // { dg-message "in .constexpr." }
+constexpr bool i2 = test_modification<int>();  // { dg-message "in .constexpr." }
+constexpr bool i3 = test_scope<int>();         // { dg-message "in .constexpr." }
+constexpr bool i4 = test_destroy_temp<int>();  // { dg-message "in .constexpr." "" { xfail *-*-* } }
+constexpr bool i5 = test_parameter(int{});     // { dg-error "destroying" }
+constexpr bool i6 = test_bindings<int>();
+
+struct Trivial { int x; };
+constexpr bool t1 = test_access<Trivial>();        // { dg-message "in .constexpr." }
+constexpr bool t2 = test_modification<Trivial>();  // { dg-message "in .constexpr." }
+constexpr bool t3 = test_scope<Trivial>();         // { dg-message "in .constexpr." }
+constexpr bool t4 = test_destroy_temp<Trivial>();  // { dg-message "in .constexpr." }
+constexpr bool t5 = test_parameter(Trivial{});     // { dg-error "destroying" }
+constexpr bool t6 = test_bindings<Trivial>();
+
+#if __cplusplus >= 202002L
+struct NonTrivial { int x; constexpr ~NonTrivial() {} };  // { dg-error "destroying" "" { target c++20 } }
+constexpr bool n1 = test_access<NonTrivial>();        // { dg-message "in .constexpr." "" { target c++20 } }
+constexpr bool n2 = test_modification<NonTrivial>();  // { dg-message "in .constexpr." "" { target c++20 } }
+constexpr bool n3 = test_scope<NonTrivial>();         // { dg-message "in .constexpr." "" { target c++20 } }
+constexpr bool n4 = test_destroy_temp<NonTrivial>();  // { dg-message "in .constexpr." "" { target c++20 } }
+constexpr bool n5 = test_parameter(NonTrivial{});     // { dg-error "destroying" "" { target c++20 } }
+constexpr bool n6 = test_bindings<NonTrivial>();
+#endif
+
diff --git a/gcc/testsuite/g++.dg/cpp2a/bitfield2.C b/gcc/testsuite/g++.dg/cpp2a/bitfield2.C
index dcb424fc8f6..885d4f0e26d 100644
--- a/gcc/testsuite/g++.dg/cpp2a/bitfield2.C
+++ b/gcc/testsuite/g++.dg/cpp2a/bitfield2.C
@@ -13,7 +13,7 @@  template <bool V, int W>
 struct U {
   int j : W = 7;		// { dg-warning "default member initializers for bit-fields only available with" "" { target c++17_down } }
   int k : W { 8 };		// { dg-warning "default member initializers for bit-fields only available with" "" { target c++17_down } }
-  int l : V ? 7 : a = 3;	// { dg-error "modification of .a. is not a constant expression" }
+  int l : V ? 7 : a = 3;	// { dg-error "modification of .a. from outside current evaluation is not a constant expression" }
 				// { dg-error "width not an integer constant" "" { target *-*-* } .-1 }
   int m : (V ? W : b) = 9;	// { dg-warning "default member initializers for bit-fields only available with" "" { target c++17_down } }
 				// { dg-error "zero width for bit-field" "" { target *-*-* } .-1 }
diff --git a/gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime1.C b/gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime1.C
new file mode 100644
index 00000000000..36163844eca
--- /dev/null
+++ b/gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime1.C
@@ -0,0 +1,21 @@ 
+// { dg-do compile { target c++20 } }
+
+#include "construct_at.h"
+
+struct S { int x; };
+constexpr int f() {
+  S s;
+  s.~S();
+  std::construct_at(&s, 5);
+  return s.x;
+}
+static_assert(f() == 5);
+
+struct T { int x; constexpr ~T() {} };
+constexpr int g() {
+  T t;
+  t.~T();
+  std::construct_at(&t, 12);
+  return t.x;
+}
+static_assert(g() == 12);
diff --git a/gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime2.C b/gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime2.C
new file mode 100644
index 00000000000..56cc9e3c1c8
--- /dev/null
+++ b/gcc/testsuite/g++.dg/cpp2a/constexpr-lifetime2.C
@@ -0,0 +1,23 @@ 
+// { dg-do compile { target c++20 } }
+
+#include "construct_at.h"
+
+struct S { int x; };
+
+constexpr bool foo(S s, S*& p) {
+  p = &s;
+  s.~S();
+  return true;
+}
+
+constexpr bool bar() {
+  // This is, strictly speaking, implementation-defined behaviour;
+  // see [expr.call] p6.  However, in all other cases we destroy
+  // at the end of the full-expression, so the below should be fixed.
+  S* p;
+  foo(S{}, p), std::construct_at(p);  // { dg-bogus "destroying" "" { xfail *-*-* } }
+
+  return true;
+}
+
+constexpr bool x = bar();
diff --git a/gcc/testsuite/g++.dg/cpp2a/constexpr-new3.C b/gcc/testsuite/g++.dg/cpp2a/constexpr-new3.C
index 3ba440fec53..5d9f192507b 100644
--- a/gcc/testsuite/g++.dg/cpp2a/constexpr-new3.C
+++ b/gcc/testsuite/g++.dg/cpp2a/constexpr-new3.C
@@ -34,7 +34,7 @@  constexpr auto v3 = f3 ();	// { dg-message "in 'constexpr' expansion of" }
 constexpr bool
 f4 (int *p)
 {
-  delete p;			// { dg-error "deallocation of storage that was not previously allocated" }
+  delete p;			// { dg-error "destroying 'q' from outside current evaluation" }
   return false;
 }
 
@@ -70,3 +70,18 @@  f7 ()
 }
 
 constexpr auto v7 = f7 ();
+
+constexpr bool
+f8_impl (int *p)
+{
+  delete p;			// { dg-error "deallocation of storage that was not previously allocated" }
+  return false;
+}
+
+constexpr bool
+f8 ()
+{
+  int q = 0;
+  return f8_impl (&q);
+}
+constexpr auto v8 = f8 ();	// { dg-message "in 'constexpr' expansion of" }