On 8/29/25 8:54 AM, Jakub Jelinek wrote:
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.

I think that's right; this paper really breaks the equivalence between static and constexpr initialization, it makes sense to switch to a front-end predicate. "constexpr-representable" seems like a good candidate for that predicate. I'd think it makes sense to rename reduced_... instead of adding a separate function.

It would be nice to move the CONSTRUCTOR_NO_CLEARING modification out of that function, and maybe only check it there when CHECKING_P.

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

I guess we can use the same predicate above. Maybe optimized with a flag on CONSTRUCTOR so we don't need to recurse very far?

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

Whether the extended temporary is constexpr-representable affects whether the reference variable is constexpr-initializable, which is required by the standard for a constexpr declaration. How and when we check those properties is an internal matter; if it's easiest to check for the extended temporary and then reuse that result when checking the reference, the standard doesn't care.

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,

No, the 42 temporary passed to a function call is not extended.

    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 relevant implementation is in mark_use; it shouldn't need special changes, but it relies on the same pattern of calling maybe_constant_value followed by checking TREE_CONSTANT on the result discussed in point 2.

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