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