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