gentle ping, apparently folks are interested in getting a solution landed.
thanks
Iain

> On 6 Oct 2026, at 23:02, Iain Sandoe <[email protected]> wrote:
> 
> Tested on x86_64-linux, darwin, OK for trunk, and eventual backports
> to fix the 126811 regression on open branches?
> thanks
> Iain
> 
> --- 8< ---
> 
> The initial solution proposed and landed for 126811 was to avoid calling
> tsubst on constructors (motivated by a similar scheme used for constructor
> elements).  The intent was to avoid recursing into these where they have
> NSDMIs (or other content that contains expressions that tsubst does not
> expect).  The concern at the time was that to handle an arbitrary set of
> such conditions in tsubst would be open-ended and hard to prove complete.
> 
> However, the solution landed has just pushed that problem onto constexpr
> handling and resulted in a number of reported regressions.  Rather than
> trying to fix these issues in constexpr, a rework of the patch for the
> original (126811) problem deals with the cases in which tsubst is expected
> to be visited by expressions already lowered.
> 
> One specific expected case is when tsubst is called with NULL_TREE args
> and is used explicitly to rewrite the tree.  We also handle cases now
> where the constructor tree contains a mixture of sub-trees that still
> require substitution.
> 
> In the final analysis the penalty for doing this in tsubst is small -
> firstly the current set of excluded trees are (presumably) never reached
> in current code (an absence of reports).  The case that is reached is
> actually guarded by 'checking' and therefore only enabled for trunk.  We
> can also short-circuit for the special case that the substitution is called
> with null args - so that the original concern about complexity in tsubst
> turned out to be somewhat unfounded.
> 
> The patch reverts the change previously applied for 126811 so that we do
> not reopen that regression at the same time.  The combination of gating
> on checking and that the current reporting conditions are not reached
> should make this reasonable for fixing the 126811 regression on open
> branches.
> 
> My original testsuite additions were augmented with the assistance of an
> LLM given the guidance that we should intend comprehensive coverage,
> therefore they are partially LLM-generated.
> 
> PR c++/126811
> PR c++/127395
> PR c++/127439
> PR c++/127573
> 
> gcc/cp/ChangeLog:
> 
> * pt.cc (tsubst_expr): Allow for CONSTRUCTORS that may have a
> mixture of lowered and to-be-substituted sub-trees.
> * typeck2.cc (store_init_value): Revert the gating of tsubst
> for CONSTRUCTOR trees.
> 
> gcc/testsuite/ChangeLog:
> 
> * g++.dg/cpp0x/constexpr-scalar-init1.C: New test.
> * g++.dg/cpp0x/nsdmi-template28.C: New test.
> 
> Signed-off-by: Iain Sandoe <[email protected]>
> Assisted-by: Claude Opus 5 <[email protected]>
> ---
> gcc/cp/pt.cc                                  |  25 +++-
> gcc/cp/typeck2.cc                             |   9 +-
> .../g++.dg/cpp0x/constexpr-scalar-init1.C     | 131 ++++++++++++++++++
> gcc/testsuite/g++.dg/cpp0x/nsdmi-template28.C |  82 +++++++++++
> 4 files changed, 236 insertions(+), 11 deletions(-)
> create mode 100644 gcc/testsuite/g++.dg/cpp0x/constexpr-scalar-init1.C
> create mode 100644 gcc/testsuite/g++.dg/cpp0x/nsdmi-template28.C
> 
> diff --git a/gcc/cp/pt.cc b/gcc/cp/pt.cc
> index 2b68ceb0cd2..1953a579a20 100644
> --- a/gcc/cp/pt.cc
> +++ b/gcc/cp/pt.cc
> @@ -21945,7 +21945,13 @@ tsubst_expr (tree t, tree args, tsubst_flags_t 
> complain, tree in_decl)
>       }
> 
>     case FIX_TRUNC_EXPR:
> -      /* convert_like should have created an IMPLICIT_CONV_EXPR.  */
> +      /* convert_like should have created an IMPLICIT_CONV_EXPR, unless we
> + are on the fold path (ARGS == NULL_TREE), where we can get elements
> + that were folded outside any template (e.g an NSDMI). In those cases
> + there is nothing to substitute into them, but seeing one is not an
> + error.  */
> +      if (args == NULL_TREE || !instantiation_dependent_expression_p (t))
> + RETURN (t);
>       gcc_unreachable ();
> 
>     case ADDR_EXPR:
> @@ -23379,12 +23385,17 @@ tsubst_expr (tree t, tree args, tsubst_flags_t 
> complain, tree in_decl)
>     case CLEANUP_POINT_EXPR:
>       /* We shouldn't have built any of these during initial template
> generation.  Instead, they should be built during instantiation
> - in response to the saved STMT_IS_FULL_EXPR_P setting.  */
> + in response to the saved STMT_IS_FULL_EXPR_P setting - unless we
> + are on the fold path; see FIX_TRUNC_EXPR above.  */
> +      if (args == NULL_TREE || !instantiation_dependent_expression_p (t))
> + RETURN (t);
>       gcc_unreachable ();
> 
>     case TARGET_EXPR:
>       /* TARGET_EXPR represents temporary objects and should not appear in
> - templated trees.  */
> + templated trees -- but see CLEANUP_POINT_EXPR above.  */
> +      if (args == NULL_TREE || !instantiation_dependent_expression_p (t))
> + RETURN (t);
>       gcc_unreachable ();
> 
>     case OFFSET_REF:
> @@ -23663,8 +23674,12 @@ tsubst_expr (tree t, tree args, tsubst_flags_t 
> complain, tree in_decl)
>       if (tree subst = objcp_tsubst_expr (t, args, complain, in_decl))
> RETURN (subst);
> 
> -      /* We shouldn't get here, but keep going if !flag_checking.  */
> -      if (flag_checking)
> +      /* We shouldn't get here, but keep going if !flag_checking.  On the 
> fold
> + path (ARGS == NULL_TREE) an already-folded tree has nothing to
> + substitute; see FIX_TRUNC_EXPR above.  */
> +      if (flag_checking
> +  && args != NULL_TREE
> +  && instantiation_dependent_expression_p (t))
> gcc_unreachable ();
>       RETURN (t);
>     }
> diff --git a/gcc/cp/typeck2.cc b/gcc/cp/typeck2.cc
> index c7ebd080edc..08ba3da53d6 100644
> --- a/gcc/cp/typeck2.cc
> +++ b/gcc/cp/typeck2.cc
> @@ -1072,12 +1072,9 @@ store_init_value (tree decl, tree init, vec<tree, 
> va_gc>** cleanups, int flags)
>  || (DECL_IN_AGGR_P (decl)
>      && DECL_INITIALIZED_IN_CLASS_P (decl)))
> {
> -  /* As in massage_init_elt, do not fold a CONSTRUCTOR in a template
> -     it has already been folded c++/126811.  */
> -  if (!(processing_template_decl && TREE_CODE (value) == CONSTRUCTOR))
> -    value = fold_non_dependent_expr (value, tf_warning_or_error,
> -     /*manifestly_const_eval=*/true,
> -     decl);
> +  value = fold_non_dependent_expr (value, tf_warning_or_error,
> +   /*manifestly_const_eval=*/true,
> +   decl);
>  if (value == error_mark_node)
>    ;
>  /* Diagnose a non-constant initializer for constexpr variable or
> diff --git a/gcc/testsuite/g++.dg/cpp0x/constexpr-scalar-init1.C 
> b/gcc/testsuite/g++.dg/cpp0x/constexpr-scalar-init1.C
> new file mode 100644
> index 00000000000..5a58c6b1642
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/cpp0x/constexpr-scalar-init1.C
> @@ -0,0 +1,131 @@
> +// PR c++/126811 c++/127395
> +// { dg-do compile { target c++11 } }
> +
> +// The static_asserts ensure that we actually test the pathway.
> +
> +enum E { e0 = 0, e7 = 7 };
> +struct C { int m; int n; void mf (); };
> +using P = int *;
> +using PM = int C::*; // scalar: takes the failing path
> +using PMF = void (C::*) (); // RECORD_TYPE, so a different path
> +using CD = _Complex double; // SCALAR_TYPE_P via ARITHMETIC_TYPE_P
> +typedef int V __attribute__((vector_size (16)));
> +typedef float VF __attribute__((vector_size (16)));
> +
> +template <int> void scalars ()
> +{
> +  constexpr int i0 = int{};
> +  constexpr int i7 = int{7};
> +  constexpr char c0 = char{};
> +  constexpr bool b0 = bool{};
> +  constexpr bool b1 = bool{true};
> +  constexpr float f0 = float{};
> +  constexpr float f2 = float{2};
> +  constexpr double d0 = double{};
> +  constexpr double d1 = double{1};
> +  constexpr E ee = E{};
> +  constexpr P pp = P{};
> +  constexpr PM pm = PM{};
> +  constexpr CD cd = CD{};
> +
> +  static_assert (i0 == 0, "int{}");
> +  static_assert (i7 == 7, "int{7}");
> +  static_assert (c0 == 0, "char{}");
> +  static_assert (b0 == false, "bool{}");
> +  static_assert (b1 == true, "bool{true}");
> +  static_assert (f0 == 0.0f, "float{}");
> +  static_assert (f2 == 2.0f, "float{2}");
> +  static_assert (d0 == 0.0, "double{}");
> +  static_assert (d1 == 1.0, "double{1}");
> +  static_assert (ee == e0, "E{}");
> +  static_assert (pp == nullptr, "P{}");
> +  static_assert (pm == nullptr, "PM{}");
> +  static_assert (__real cd == 0.0 && __imag cd == 0.0, "_Complex double{}");
> +}
> +
> +template <int> void ptrmem ()
> +{
> +  constexpr PM null = PM{}; // empty: needs build_value_init,
> +  constexpr PM nullp = PM{nullptr}; // not build_zero_cst
> + // one element: needs the C++
> + // conversion, not fold_convert
> +  constexpr PM first = &C::m;
> +  constexpr PM second = PM{&C::n};
> +
> +  static_assert (null == nullptr, "PM{}");
> +  static_assert (nullp == nullptr, "PM{nullptr}");
> +  static_assert (null != first, "null PM is not offset 0");
> +  static_assert (second == &C::n, "PM{&C::n}");
> +}
> +
> +template <int> void vectors ()
> +{
> +  constexpr V v0 = V{};
> +  constexpr V v4 = V{1,2,3,4};
> +  constexpr VF vf0 = VF{};
> +  static_assert (v0[0] == 0, "V{}");
> +  static_assert (v4[0] == 1 && v4[3] == 4, "V{1,2,3,4}");
> +  static_assert (vf0[2] == 0.0f, "VF{}");
> +}
> +
> +template <int> void ptrmemfunc ()
> +{
> +  constexpr PMF p = PMF{};
> +  static_assert (p == nullptr, "PMF{}");
> +}
> +
> +struct S { int x; double y; };
> +
> +template <int> void aggregates ()
> +{
> +  constexpr int a4[4] = {1,2,3,4};
> +  constexpr int a0[4] = {};
> +  constexpr S s = S{1,2.0};
> +  constexpr S sz = S{};
> +  static_assert (a4[0] == 1 && a4[3] == 4, "int[4] = {1,2,3,4}");
> +  static_assert (a0[0] == 0 && a0[3] == 0, "int[4] = {}");
> +  static_assert (s.x == 1 && s.y == 2.0, "S{1,2.0}");
> +  static_assert (sz.x == 0 && sz.y == 0.0, "S{}");
> +}
> +
> +// The fix should not touch this.
> +template <int N> void dependent ()
> +{
> +  constexpr int i = int{N};
> +  constexpr double d = double{N};
> +  static_assert (i == 3, "int{N}");
> +  static_assert (d == 3.0, "double{N}");
> +}
> +
> +// or this...
> +template <int> void not_constexpr ()
> +{
> +  int i = int{5};
> +  double d = double{};
> +  V v = V{1,2,3,4};
> +  (void) i; (void) d; (void) v;
> +}
> +
> +// CONSTRUCTOR should not reach the effected path.
> +void nontemplate ()
> +{
> +  constexpr int i7 = int{7};
> +  constexpr double d1 = double{1};
> +  constexpr PM pm = PM{};
> +  constexpr V v4 = V{1,2,3,4};
> +  static_assert (i7 == 7, "int{7} outside a template");
> +  static_assert (d1 == 1.0, "double{1} outside a template");
> +  static_assert (pm == nullptr, "PM{} outside a template");
> +  static_assert (v4[3] == 4, "V{1,2,3,4} outside a template");
> +}
> +
> +void use ()
> +{
> +  scalars<0> ();
> +  ptrmem<0> ();
> +  vectors<0> ();
> +  ptrmemfunc<0> ();
> +  aggregates<0> ();
> +  dependent<3> ();
> +  not_constexpr<0> ();
> +}
> diff --git a/gcc/testsuite/g++.dg/cpp0x/nsdmi-template28.C 
> b/gcc/testsuite/g++.dg/cpp0x/nsdmi-template28.C
> new file mode 100644
> index 00000000000..3c74947376d
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/cpp0x/nsdmi-template28.C
> @@ -0,0 +1,82 @@
> +// PR c++/126811 c++/127439 c++/127593
> +// { dg-do compile { target c++11 } }
> +
> +// This covers a CONSTRUCTOR holding both
> +//   * an already-folded NSDMI sub-tree
> +//   * an undigested IMPLICIT_CONV_EXPR
> +
> +struct A { double m = 1; }; // NSDMI -> FLOAT_EXPR once digested
> +
> +// The constructor is deliberately DECLARED BUT NOT DEFINED: that is what
> +// leaves the conversion from "" 
> +
> +struct a { constexpr a (const char *); }; // { dg-warning {inline function 
> 'constexpr a::a.const char..' used but never defined} }
> +
> +struct Mix { A x; a y; };
> +
> +template <typename> void copy_list_init ()
> +{
> +  constexpr Mix m = { {}, "" };
> +  (void) m;
> +}
> +
> +template <typename> void direct_list_init ()
> +{
> +  constexpr Mix m{ {}, "" };
> +  (void) m;
> +}
> +
> +// Nested case.
> +template <typename> void nested ()
> +{
> +  constexpr A x[]{ {} };
> +  (void) x;
> +}
> +
> +// opposite requirement in the same syntax.
> +template <typename> void array_of_class ()
> +{
> +  constexpr a c[] = { "" };
> +  (void) c;
> +}
> +
> +#if __cplusplus >= 202002L
> +// Designated initializers put the two halves at the SAME level with nothing
> +// nested: `m` comes from the NSDMI, `y` from the conversion.
> +struct Mix2 { double m = 1; a y; };
> +
> +template <typename> void designated ()
> +{
> +  constexpr Mix2 v{ .y = "" };
> +  (void) v;
> +}
> +#endif
> +
> +// Value coverage for the same shape with a DEFINED constructor, so the
> +// initializer really is constant-evaluated and the result can be checked.
> +// This is the case a fix must not silently break while stopping the ICEs.
> +struct b { constexpr b (const char *s) : p (s) {} const char *p; };
> +struct MixDef { A x; b y; };
> +
> +template <typename> void values ()
> +{
> +  constexpr MixDef m{ {}, "" };
> +  static_assert (m.x.m == 1.0, "NSDMI half");
> +  static_assert (m.y.p[0] == 0, "converted half");
> +}
> +
> +// The same mixture reached through a real instantiation takes a different 
> route
> +// inside tsubst (non-null ARGS), and used to ICE there too rather than give 
> this
> +// diagnostic.
> +template <typename> void instantiated ()
> +{
> +  constexpr Mix m = { {}, "" }; // { dg-error "used before its definition" }
> +  (void) m;
> +}
> +
> +void use ()
> +{
> +  nested<int> ();
> +  instantiated<int> ();
> +  values<int> ();
> +}
> -- 
> 2.50.1 (Apple Git-155)
> 

Reply via email to