Hi!

So, I had a look at the remaining part of C++26 P2686R4, which I think
we really should implement for GCC 16 because people are already filing
PRs about constexpr structured bindings or expansion statements not working
the way they should in some cases.

That plus the recent PR121670 and that we accept say
static constexpr int a = 4;
static constexpr const int *b = &a + 2;
when that should be rejected.

I see various problems:
1) reduced_constant_expression_p heavily uses middle-end code and is used
   for two IMHO quite different purposes, one is to decide what can be
   initialized at constant time (e.g. what can be used as initializer of
   a const value without needing to initialize it at runtime), for which
   it I think serves well but perhaps some of the CONSTRUCTOR_NO_CLEARING
   handling there is unnecessary; and then it is used in VERIFY_CONSTANT
   or to decide if DECL_INITIALIZED_BY_CONSTANT_EXPRESSION_P should be set
   e.g. in
typeck2.cc-      const_init = (reduced_constant_expression_p (value)
typeck2.cc-                 || error_operand_p (value));
typeck2.cc:      DECL_INITIALIZED_BY_CONSTANT_EXPRESSION_P (decl) = const_init;
   The problem with the latter cases is that the middle-end part happily
   skips over all kinds of casts including reinterpret casts,
   POINTER_PLUS_EXPR handling doesn't verify the resulting address still
   points into the object or at the end of it (for references not even
   to the end), etc.  Now, sure, we have cxx_eval_outermost_constant_expr
   which will reject invalid cases, but the rejection is just about
   returning the unmodified argument passed to it (if it is the
   maybe_constant_init or similar which is quiet), perhaps with something
   to drop the TREE_CONSTANT bit (but e.g. the typeck2.cc case doesn't
   care about that and when reduced_constant_expression_p is happy about it,
   it will happily accept it).
   In the patch below I've tried to tweak reduced_constant_expression_p to
   allow the C++26 references to automatic variables if they are from
   current_function_decl, but perhaps we should split it up, have
   reduced_constant_expression_p be used for the cases where we decide
   can this static var be initialized statically or needs dynamic
   initializer, and use constexpr_representable_p as C++ FE specific
   implementation whether some expression is a valid initializer in the
   constexpr sense, i.e. verify pointers/references are within bounds,
   disallow reinterpret casts, for C++26 allow references to automatic
   variables in the same frame, etc.
2) various spots also do use TREE_CONSTANT bit (in the FE and middle-end).
   Now, addresses of automatic vars even in the same frame aren't
   TREE_CONSTANT from the middle-end POV, so either we violate it
   during FE handling and fix up e.g. during genericization, or use
   some new flag (TREE_LANG_FLAG_7 if we add it, there are spare bits),
   or use a predicate instead of the FE TREE_CONSTANT checks
3) as mentioned in a comment in the patch, I wonder what is the
   purpose of including in constituent values and references also
   constituent values/references from lifetime extended temporary
   object; aren't those checked normally when they are created
   (that they are constexpr-representable in the current function)
   or is there some reason to verify them again when referenced
   from some maybe-constexpr-representable variable?  If we need
   it, we need some way to differentiate between VAR_DECLs for those
   lifetime extended temporaries vs. other vars
4) the paper says that one can refer to automatic variables in the
   same frame and to lifetime extended temporaries (but presumably
   not other temporaries).  Initially I thought it would be
   taking address of TARGET_EXPR is not valid, taking address of
   VAR_DECL in the same frame is ok.  But in:
struct S { const int *p; const int &q; constexpr S (const int &x) : p (&x), q 
(x) {} };
struct T { const int *p; constexpr T (const int &x) : p (&x) {} };

void
foo ()
{
  constexpr S s (42);
  constexpr T t (42);
}
   I'd think that the s variable is constexpr-representable, because
   of the s.q reference causing lifetime extension of the 42 temporary
   and then both s.p can point to that and s.q can refer to that,
   while t is not constexpr-representable because the lifetime of
   the temporary is not extended and so t.p can't point to it.
   Am I wrong on that?
   But, in the expressions that VERIFY_CONSTANT (e.g. at the end of
   cxx_eval_outermost_constant_expression) sees, it uses ADDR_EXPR of
   artificial VAR_DECLs in all cases, and furthermore I think the
   constant verification is done prior to the temporaries being
   lifetime extended, so not sure how to differentiate between
   "this VAR_DECL is a normal var vs. this VAR_DECL is a TARGET_EXPR
   slot and has not been lifetime extended and won't be vs.
   this VAR_DECL has been lifetime extended vs. this VAR_DECL is
   a TARGET_EXPR slot and will be lifetime extended 
5) the paper contains also some basic.def.odr changes but I don't
   see where in DECL_ODR_USED guarding code we actually test such
   thing, shall that be just changed somewhere in the lambda
   code when deciding what to capture and what shouldn't be captured?

The testcase in the patch of course fails, although it sets
DECL_INITIALIZED_BY_CONSTANT_EXPRESSION_P on the VAR_DECLs,
when trying to use those in constant expressions we check TREE_CONSTANT
and fail when it is not set.

Thoughts on this?

--- gcc/cp/constexpr.cc.jj      2025-08-27 13:58:12.713075137 +0200
+++ gcc/cp/constexpr.cc 2025-08-27 14:19:19.310452830 +0200
@@ -4383,6 +4383,49 @@ cxx_eval_call_expression (const constexp
   return result;
 }
 
+/* Return true if T is constexpr-representable at some point in
+   current_function_decl.  REF_P is true if it is a reference.  */
+
+static bool
+constexpr_representable_p (tree t, bool ref_p)
+{
+  switch (TREE_CODE (t))
+    {
+    case ADDR_EXPR:
+      tree o;
+      o = TREE_OPERAND (t, 0);
+      while (TREE_CODE (o) == COMPONENT_REF
+            || (TREE_CODE (o) == ARRAY_REF
+                && TREE_CODE (TREE_OPERAND (o, 1)) == INTEGER_CST)
+            || TREE_CODE (o) == REALPART_EXPR
+            || TREE_CODE (o) == IMAGPART_EXPR)
+       o = TREE_OPERAND (o, 0);
+      if (DECL_P (o) && DECL_CONTEXT (o) == current_function_decl)
+       {
+         /* FIXME: https://eel.is/c++draft/expr.const#3.sentence-2
+            "and references of that temporary object are also constituent
+            values and references of x, recursively"
+            Do we need to somehow mark VAR_DECLs created by
+            make_temporary_var_for_ref_to_temp and recurse on their
+            DECL_INITIAL?  */
+         return true;
+       }
+      return initializer_constant_valid_p (t, TREE_TYPE (t)) != NULL_TREE;
+    case POINTER_PLUS_EXPR:
+      if (initializer_constant_valid_p (TREE_OPERAND (t, 1), sizetype)
+         == NULL_TREE)
+       return false;
+      return constexpr_representable_p (TREE_OPERAND (t, 0), ref_p);
+    CASE_CONVERT:
+      if (!POINTER_TYPE_P (TREE_TYPE (TREE_OPERAND (t, 0))))
+       return initializer_constant_valid_p (t, TREE_TYPE (t)) != NULL_TREE;
+      ref_p = ref_p || TYPE_REF_P (TREE_TYPE (TREE_OPERAND (t, 0)));
+      return constexpr_representable_p (TREE_OPERAND (t, 0), ref_p);
+    default:
+      return initializer_constant_valid_p (t, TREE_TYPE (t)) != NULL_TREE;
+    }
+}
+
 /* Return true if T is a valid constant initializer.  If a CONSTRUCTOR
    initializes all the members, the CONSTRUCTOR_NO_CLEARING flag will be
    cleared.  If called recursively on a FIELD_DECL's CONSTRUCTOR, SZ
@@ -4499,10 +4542,26 @@ ok:
        CONSTRUCTOR_NO_CLEARING (t) = false;
       return true;
 
+    case POINTER_PLUS_EXPR:
+    case ADDR_EXPR:
+      if (cxx_dialect < cxx26 || current_function_decl == NULL_TREE)
+       break;
+      return constexpr_representable_p (t, TYPE_REF_P (TREE_TYPE (t)));
+
+    CASE_CONVERT:
+      if (cxx_dialect < cxx26
+         || current_function_decl == NULL_TREE
+         || !POINTER_TYPE_P (TREE_TYPE (t))
+         || !POINTER_TYPE_P (TREE_TYPE (TREE_OPERAND (t, 0))))
+       break;
+      return constexpr_representable_p (t, TYPE_REF_P (TREE_TYPE (t)));
+
     default:
-      /* FIXME are we calling this too much?  */
-      return initializer_constant_valid_p (t, TREE_TYPE (t)) != NULL_TREE;
+      break;
     }
+
+  /* FIXME are we calling this too much?  */
+  return initializer_constant_valid_p (t, TREE_TYPE (t)) != NULL_TREE;
 }
 
 /* *TP was not deemed constant by reduced_constant_expression_p.  Explain
--- gcc/testsuite/g++.dg/cpp26/constexpr-ref1.C.jj      2025-08-27 
17:42:06.192755642 +0200
+++ gcc/testsuite/g++.dg/cpp26/constexpr-ref1.C 2025-08-27 17:41:58.434851394 
+0200
@@ -0,0 +1,35 @@
+// C++26 P2686R4 - references to constexpr variables
+// { dg-do compile { target c++26 } }
+
+void
+foo ()
+{
+  constexpr int a = 1;
+  constexpr auto *b = &a;
+  static_assert (*b == 1 && b == &a);
+  static int c = 2;
+  static constexpr int &d = c;
+  static_assert (&d == &c);
+  int e = 3;
+  constexpr int &f = e;
+  static_assert (&f == &e);
+}
+
+struct A { int a; const int &b; };
+
+void
+bar ()
+{
+  static int c;
+  int d;
+  A e = { 1, 2 };
+  static A f = { 3, 4 };
+  constexpr const int *g[] = { &c, &d, &e.a, &f.a, &e.b, &f.b };
+  static_assert (g[0] == &c && g[1] == &d && g[2] == &e.a
+                && g[3] == &f.a && g[4] == &e.b && g[5] == &f.b);
+  auto h = [] {
+    int i;
+    constexpr const int *j[] = { &c, &f.a, &i, &f.b };
+    static_assert (j[0] == &c && j[1] == &f.a && j[2] == &i && j[3] == &f.b);
+  };
+}

        Jakub

Reply via email to