[PATCH 3/3] lto: Keep vtable definition for PMF calls

Chi Wang <[email protected]>
Newsgroups gmane.comp.gcc.patches
Message-ID <[email protected]>
This patch extends the previous IPA optimizations into WPA. For a PMF
call to be successfully devirtualized, the relevant vtable should also
be available in WPA for the pass to reference.

WPA will retain relevant vtable group definitions for a PMF edge until
IPA-inlining and IPA-CP have finished. Before these optimizations, WPA
will keep all vtable group definitions of each derived type alive for
them to successfully look up. In practice, most vtables are either
loaded or forced by ABI, so it should not cause significant memory
bloat. However, IPA-fnsummary is changed to clean up the unused vtable
after the inlining info has been cleared. This is important to make sure
that LTRANS does not produce unused vtables into the compiled binary.

The implementation adds a flag to the varpool nodes to track whether
the node is a vtable group and might still be used by a PMF indirect
call.

gcc/ChangeLog:

	* cgraph.h (struct varpool_node): Add ptrmemfunc_vtable.
	(varpool_node::all_refs_explicit_p): Take ptrmemfunc_vtable
	into consideration.
	* cgraphunit.cc (walk_ptrmemfunc_vtable_targets): New function.
	Walk the derived type and mark the vtables as reachable.
	(analyze_functions): Call walk_ptrmemfunc_vtable_targets for
	PMF calls to retain vtables.
	* ipa-devirt.cc (possible_ptrmemfunc_vtable_targets_1): New
	function. Recursive walker to enumerate derived type vtable
	groups.
	(possible_ptrmemfunc_vtable_targets): New function. Enumerating
	derived type vtable groups.
	* ipa-fnsummary.cc (pass_ipa_free_fn_summary::execute): Add
	TODO_remove_functions for the second pass.
	* ipa-utils.h (possible_ptrmemfunc_vtable_targets): New
	function.
	* ipa.cc (walk_ptrmemfunc_vtable_targets): New function. Mark
	derived type vtable groups as reachable.
	(symbol_table::remove_unreachable_nodes): Make vtables reachable
	from PMF indirect edge.
	* lto-cgraph.cc (compute_ltrans_boundary): Streaming of vtables
	with definition still available for PMF edges when
	-fdevirtualize-at-ltrans is set.
	* varpool.cc (varpool_node::dump): Dump the new flag
	ptrmemfunc_vtable.

gcc/testsuite/ChangeLog:

	* g++.dg/lto/devirtpmf1_0.C: New test.
	* g++.dg/lto/devirtpmf1_1.C: New test.
	* g++.dg/lto/devirtpmf2_0.C: New test.
	* g++.dg/lto/devirtpmf2_1.C: New test.
	* g++.dg/lto/devirtpmf2_2.C: New test.
	* g++.dg/lto/devirtpmf3_0.C: New test.
	* g++.dg/lto/devirtpmf3_1.C: New test.

Signed-off-by: Chi Wang <[email protected]>
---
 gcc/cgraph.h                            | 10 ++-
 gcc/cgraphunit.cc                       | 24 ++++++++
 gcc/ipa-devirt.cc                       | 45 ++++++++++++++
 gcc/ipa-fnsummary.cc                    |  2 +-
 gcc/ipa-utils.h                         |  2 +
 gcc/ipa.cc                              | 49 +++++++++++++--
 gcc/lto-cgraph.cc                       | 82 +++++++++++++++++--------
 gcc/testsuite/g++.dg/lto/devirtpmf1_0.C | 13 ++++
 gcc/testsuite/g++.dg/lto/devirtpmf1_1.C | 16 +++++
 gcc/testsuite/g++.dg/lto/devirtpmf2_0.C | 13 ++++
 gcc/testsuite/g++.dg/lto/devirtpmf2_1.C | 11 ++++
 gcc/testsuite/g++.dg/lto/devirtpmf2_2.C |  7 +++
 gcc/testsuite/g++.dg/lto/devirtpmf3_0.C | 13 ++++
 gcc/testsuite/g++.dg/lto/devirtpmf3_1.C | 24 ++++++++
 gcc/varpool.cc                          |  2 +
 15 files changed, 277 insertions(+), 36 deletions(-)
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf1_0.C
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf1_1.C
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf2_0.C
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf2_1.C
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf2_2.C
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf3_0.C
 create mode 100644 gcc/testsuite/g++.dg/lto/devirtpmf3_1.C

diff --git a/gcc/cgraph.h b/gcc/cgraph.h
index 0724c28c3ef..3f26c001a75 100644
--- a/gcc/cgraph.h
+++ b/gcc/cgraph.h
@@ -2282,7 +2282,8 @@ struct GTY((tag ("SYMTAB_VARIABLE"))) varpool_node : public symtab_node
   /* Constructor.  */
   explicit varpool_node ()
     : symtab_node (SYMTAB_VARIABLE), output (0), dynamically_initialized (0),
-      tls_model (TLS_MODEL_NONE), used_by_single_function (0)
+      tls_model (TLS_MODEL_NONE), used_by_single_function (0),
+      ptrmemfunc_vtable (0)
   {}
 
   /* Dump given varpool node to F.  */
@@ -2402,6 +2403,10 @@ struct GTY((tag ("SYMTAB_VARIABLE"))) varpool_node : public symtab_node
      if we did not do any inter-procedural code movement.  */
   unsigned used_by_single_function : 1;
 
+  /* Set if the variable is a vtable that may be used by a pointer-to-member
+     function method lookup, and the call edges need to retain it in WPA.  */
+  unsigned ptrmemfunc_vtable : 1;
+
 private:
   /* Assemble thunks and aliases associated to varpool node.  */
   void assemble_aliases (void);
@@ -3590,7 +3595,8 @@ varpool_node::all_refs_explicit_p ()
 	  && !externally_visible
 	  && !used_from_other_partition
 	  && !ref_by_asm
-	  && !force_output);
+	  && !force_output
+	  && !ptrmemfunc_vtable);
 }
 
 struct tree_descriptor_hasher : ggc_ptr_hash<constant_descriptor_tree>
diff --git a/gcc/cgraphunit.cc b/gcc/cgraphunit.cc
index 67aaebf108b..2f26da58fa4 100644
--- a/gcc/cgraphunit.cc
+++ b/gcc/cgraphunit.cc
@@ -1091,6 +1091,26 @@ walk_polymorphic_call_targets (hash_set<void *> *reachable_call_targets,
     }
 }
 
+/* EDGE is a pointer to member function call.  Mark all possible vtables
+   as reachable.  REACHABLE_VTS collects vtables we already walked to
+   avoid duplicate work.  */
+
+static void
+walk_ptrmemfunc_vtable_targets (hash_set<tree> &reachable_vts,
+				cgraph_edge *edge)
+{
+  cgraph_ptrmemfunc_indirect_info *pmf
+    = as_a<cgraph_ptrmemfunc_indirect_info *> (edge->indirect_info);
+  if (!pmf->method_type)
+    return;
+  tree base = TYPE_METHOD_BASETYPE (pmf->method_type);
+  auto_vec<tree> vts
+    = possible_ptrmemfunc_vtable_targets (reachable_vts, base, pmf->outer_type);
+  for (unsigned int i = 0; i < vts.length (); i++)
+    if (varpool_node *vnode = varpool_node::get (vts[i]))
+      enqueue_node (vnode);
+}
+
 /* Issue appropriate warnings for the global declaration DECL.  */
 
 static void
@@ -1180,6 +1200,7 @@ analyze_functions (bool first_time)
   cgraph_node *first_handled = first_analyzed;
   varpool_node *first_handled_var = first_analyzed_var;
   hash_set<void *> reachable_call_targets;
+  hash_set<tree> reachable_vts;
 
   symtab_node *node;
   symtab_node *next;
@@ -1306,6 +1327,9 @@ analyze_functions (bool first_time)
 			  (edge->indirect_info))
 			walk_polymorphic_call_targets (&reachable_call_targets,
 						       edge);
+		      if (is_a<cgraph_ptrmemfunc_indirect_info *>
+			  (edge->indirect_info))
+			walk_ptrmemfunc_vtable_targets (reachable_vts, edge);
 		    }
 		}
 
diff --git a/gcc/ipa-devirt.cc b/gcc/ipa-devirt.cc
index f899959bbd3..eae35a1b046 100644
--- a/gcc/ipa-devirt.cc
+++ b/gcc/ipa-devirt.cc
@@ -3450,6 +3450,51 @@ possible_polymorphic_call_targets (tree otr_type,
   return nodes;
 }
 
+/* Recursive walker of possible_ptrmemfunc_vtable_targets.  */
+static void
+possible_ptrmemfunc_vtable_targets_1 (hash_set<tree> &reachable_vt_decls,
+				      auto_vec<tree> &vt_decls, tree base)
+{
+  base = TYPE_MAIN_VARIANT (base);
+  odr_type type = get_odr_type (base, true);
+  base = type ? type->type : base;
+
+  if (reachable_vt_decls.contains (base))
+    return;
+  reachable_vt_decls.add (base);
+
+  tree binfo = TYPE_BINFO (base);
+  if (binfo && polymorphic_type_binfo_p (binfo))
+    {
+      tree vt;
+      unsigned HOST_WIDE_INT offset;
+      if (vtable_pointer_value_to_vtable (BINFO_VTABLE (binfo), &vt, &offset))
+	vt_decls.safe_push (vt);
+    }
+  if (type)
+    for (unsigned i = 0; i < type->derived_types.length (); i++)
+      possible_ptrmemfunc_vtable_targets_1 (reachable_vt_decls,
+					    vt_decls,
+					    type->derived_types[i]->type);
+}
+
+/* BASE is a base type of a ptrmemfunc call.  Collect all possible polymorphic
+   vtable groups that might be called based on OTR type info (so it may return
+   empty if BASE is non-polymorphic).  REACHABLE_VT_DECLS collects vtables we
+   already walked to avoid duplicate work.  If OUTER is not null, also collect
+   that of OUTER (in case BASE is non-polymorphic or the derived info is not
+   complete).  */
+auto_vec<tree>
+possible_ptrmemfunc_vtable_targets (hash_set<tree> &reachable_vt_decls,
+				    tree base, tree outer)
+{
+  auto_vec<tree> vt_decls;
+  possible_ptrmemfunc_vtable_targets_1 (reachable_vt_decls, vt_decls, base);
+  if (outer)
+    possible_ptrmemfunc_vtable_targets_1 (reachable_vt_decls, vt_decls, outer);
+  return vt_decls;
+}
+
 bool
 add_decl_warning (const tree &key ATTRIBUTE_UNUSED, const decl_warn_count &value,
 		  vec<const decl_warn_count*> *vec)
diff --git a/gcc/ipa-fnsummary.cc b/gcc/ipa-fnsummary.cc
index a6ccdb1852a..f2ab59bff6e 100644
--- a/gcc/ipa-fnsummary.cc
+++ b/gcc/ipa-fnsummary.cc
@@ -5274,7 +5274,7 @@ public:
       ipa_free_all_structures_after_iinln ();
       if (!flag_wpa)
 	ipa_free_size_summary ();
-      return 0;
+      return small_p ? 0 : TODO_remove_functions;
     }
 
 private:
diff --git a/gcc/ipa-utils.h b/gcc/ipa-utils.h
index d689d45c70c..0b62cb5dcef 100644
--- a/gcc/ipa-utils.h
+++ b/gcc/ipa-utils.h
@@ -79,6 +79,8 @@ possible_polymorphic_call_targets (tree, HOST_WIDE_INT,
 				   bool *copletep = NULL,
 				   void **cache_token = NULL,
 				   bool speuclative = false);
+auto_vec<tree> possible_ptrmemfunc_vtable_targets (hash_set<tree> &, tree,
+						   tree);
 odr_type get_odr_type (tree, bool insert = false);
 bool odr_type_p (const_tree);
 bool possible_polymorphic_call_target_p (tree ref, gimple *stmt, struct cgraph_node *n);
diff --git a/gcc/ipa.cc b/gcc/ipa.cc
index 908fb301cd0..8206ee7ba8e 100644
--- a/gcc/ipa.cc
+++ b/gcc/ipa.cc
@@ -253,6 +253,35 @@ walk_polymorphic_call_targets (hash_set<void *> *reachable_call_targets,
     }
 }
 
+/* EDGE is a pointer to member function call.  Mark all possible vtables
+   as reachable and retain their definition via ptrmemfunc_vtable flag.
+   REACHABLE_VTS collects vtables we already walked to avoid duplicate work.  */
+static void
+walk_ptrmemfunc_vtable_targets (hash_set<tree> &reachable_vts,
+				cgraph_edge *edge, symtab_node **first,
+				hash_set<symtab_node *> *reachable)
+{
+  cgraph_ptrmemfunc_indirect_info *pmf
+    = as_a<cgraph_ptrmemfunc_indirect_info *> (edge->indirect_info);
+  /* We only retain the vtable if the call could still be used by ipa-prop or
+     ipa-cp.  */
+  if (pmf->param_index == -1 || ipa_node_params_sum == nullptr)
+    return;
+  if (!pmf->method_type)
+    return;
+
+  tree base = TYPE_METHOD_BASETYPE (pmf->method_type);
+  auto_vec<tree> vts
+    = possible_ptrmemfunc_vtable_targets (reachable_vts, base, pmf->outer_type);
+  for (unsigned int i = 0; i < vts.length (); i++)
+    if (varpool_node *vnode = varpool_node::get (vts[i]))
+      {
+	vnode->ptrmemfunc_vtable = true;
+	reachable->add (vnode);
+	enqueue_node (vnode, first, reachable);
+      }
+}
+
 /* Perform reachability analysis and reclaim all unreachable nodes.
 
    The algorithm is basically mark&sweep but with some extra refinements:
@@ -317,6 +346,7 @@ symbol_table::remove_unreachable_nodes (FILE *file)
   hash_set<symtab_node *> reachable;
   hash_set<tree> body_needed_for_clonning;
   hash_set<void *> reachable_call_targets;
+  hash_set<tree> reachable_vts;
 
   timevar_push (TV_IPA_UNREACHABLE);
   build_type_inheritance_graph ();
@@ -352,12 +382,15 @@ symbol_table::remove_unreachable_nodes (FILE *file)
 
   /* Mark variables that are obviously needed.  */
   FOR_EACH_DEFINED_VARIABLE (vnode)
-    if (!vnode->can_remove_if_no_refs_p()
-	&& !vnode->in_other_partition)
-      {
-	reachable.add (vnode);
-	enqueue_node (vnode, &first, &reachable);
-      }
+    {
+      vnode->ptrmemfunc_vtable = false;
+      if (!vnode->can_remove_if_no_refs_p ()
+	  && !vnode->in_other_partition)
+	{
+	  reachable.add (vnode);
+	  enqueue_node (vnode, &first, &reachable);
+	}
+    }
 
   /* Declarations or symbols in other partitions are also needed if referenced
      from asm.  */
@@ -438,6 +471,10 @@ symbol_table::remove_unreachable_nodes (FILE *file)
 		      if (usable_polymorphic_info_p (e->indirect_info))
 			walk_polymorphic_call_targets (&reachable_call_targets,
 						       e, &first, &reachable);
+		      if (is_a<cgraph_ptrmemfunc_indirect_info *>
+			  (e->indirect_info))
+			walk_ptrmemfunc_vtable_targets (reachable_vts, e,
+							&first, &reachable);
 		    }
 		}
 
diff --git a/gcc/lto-cgraph.cc b/gcc/lto-cgraph.cc
index dd902c947be..33217577a25 100644
--- a/gcc/lto-cgraph.cc
+++ b/gcc/lto-cgraph.cc
@@ -876,6 +876,7 @@ compute_ltrans_boundary (lto_symtab_encoder_t in_encoder)
   lto_symtab_encoder_t encoder;
   lto_symtab_encoder_iterator lsei;
   hash_set<void *> reachable_call_targets;
+  hash_set<tree> reachable_vts;
 
   encoder = lto_symtab_encoder_new (false);
 
@@ -961,33 +962,60 @@ compute_ltrans_boundary (lto_symtab_encoder_t in_encoder)
       /* Add all possible targets for late devirtualization.  */
       if (flag_ltrans_devirtualize || !flag_wpa)
 	for (edge = node->indirect_calls; edge; edge = edge->next_callee)
-	  if (usable_polymorphic_info_p (edge->indirect_info))
-	    {
-	      unsigned int i;
-	      void *cache_token;
-	      bool final;
-	      vec <cgraph_node *>targets
-		= possible_polymorphic_call_targets
-		    (edge, &final, &cache_token);
-	      if (cache_token != NULL
-		  && !reachable_call_targets.add (cache_token))
-		{
-		  for (i = 0; i < targets.length (); i++)
-		    {
-		      struct cgraph_node *callee = targets[i];
-
-		      /* Adding an external declarations into the unit serves
-			 no purpose and just increases its boundary.  */
-		      if (callee->definition
-			  && !lto_symtab_encoder_in_partition_p
-			       (encoder, callee))
-			{
-			  gcc_assert (!callee->inlined_to);
-			  add_node_to (encoder, callee, false);
-			}
-		    }
-		}
-	    }
+	  {
+	    if (usable_polymorphic_info_p (edge->indirect_info))
+	      {
+		unsigned int i;
+		void *cache_token;
+		bool final;
+		vec <cgraph_node *> targets
+		  = possible_polymorphic_call_targets
+		      (edge, &final, &cache_token);
+		if (cache_token != NULL
+		    && !reachable_call_targets.add (cache_token))
+		  {
+		    for (i = 0; i < targets.length (); i++)
+		      {
+			struct cgraph_node *callee = targets[i];
+
+			/* Adding an external declarations into the unit serves
+			   no purpose and just increases its boundary.  */
+			if (callee->definition
+			    && !lto_symtab_encoder_in_partition_p
+				 (encoder, callee))
+			  {
+			    gcc_assert (!callee->inlined_to);
+			    add_node_to (encoder, callee, false);
+			  }
+		      }
+		  }
+	      }
+	    else if (is_a<cgraph_ptrmemfunc_indirect_info *> (
+		       edge->indirect_info))
+	      {
+		cgraph_ptrmemfunc_indirect_info *pmf
+		  = as_a<cgraph_ptrmemfunc_indirect_info *> (
+		    edge->indirect_info);
+		if (pmf->method_type)
+		  {
+		    tree base = TYPE_METHOD_BASETYPE (pmf->method_type);
+		    auto_vec<tree> vts
+		      = possible_ptrmemfunc_vtable_targets (reachable_vts, base,
+							    pmf->outer_type);
+		    for (unsigned int i = 0; i < vts.length (); i++)
+		      {
+			varpool_node *vnode = varpool_node::get (vts[i]);
+			if (vnode && vnode->definition)
+			  {
+			    lto_symtab_encoder_encode (encoder, vnode);
+			    lto_set_symtab_encoder_encode_initializer (encoder,
+								       vnode);
+			    create_references (encoder, vnode);
+			  }
+		      }
+		  }
+	      }
+	  }
     }
   /* Be sure to also insert alias targert and thunk callees.  These needs
      to stay to aid local calling conventions.  */
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf1_0.C b/gcc/testsuite/g++.dg/lto/devirtpmf1_0.C
new file mode 100644
index 00000000000..b8450f6fd7b
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf1_0.C
@@ -0,0 +1,13 @@
+/* { dg-lto-do link } */
+/* { dg-lto-options { "-O3 -flto -fdump-ipa-cp" } } */
+/* { dg-final { scan-wpa-ipa-dump "Discovered an indirect call to a known target.*abcabc"  "cp"  } } */
+
+struct B;
+
+void
+bar (B *b, void (B::*mem) ())
+{
+  (b->*mem) ();
+}
+
+int main() {}
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf1_1.C b/gcc/testsuite/g++.dg/lto/devirtpmf1_1.C
new file mode 100644
index 00000000000..49eefba5e07
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf1_1.C
@@ -0,0 +1,16 @@
+struct A { virtual void abcabc () = 0; };
+struct B : A { void abcabc () final; };
+
+static int cnt = 0;
+
+[[gnu::noinline]] void
+B::abcabc () { cnt++; };
+
+void
+bar (B *b, void (B::*mem) ());
+
+__attribute__ ((used)) void
+baz (B *b)
+{
+  bar (b, &B::abcabc);
+}
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf2_0.C b/gcc/testsuite/g++.dg/lto/devirtpmf2_0.C
new file mode 100644
index 00000000000..45a210af585
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf2_0.C
@@ -0,0 +1,13 @@
+/* { dg-lto-do link } */
+/* { dg-lto-options { "-O3 -flto -fdump-ipa-cp -fvisibility=hidden -flto-partition=1to1" } } */
+/* { dg-final { scan-wpa-ipa-dump "Discovered an indirect call to a known target.*abcabc"  "cp"  } } */
+
+struct B;
+
+void
+bar (B *b, void (B::*mem) ())
+{
+  (b->*mem) ();
+}
+
+int main() {}
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf2_1.C b/gcc/testsuite/g++.dg/lto/devirtpmf2_1.C
new file mode 100644
index 00000000000..947bb46ddc9
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf2_1.C
@@ -0,0 +1,11 @@
+struct A { virtual void abcabc () = 0; };
+struct B : A { void abcabc () final; };
+
+void
+bar (B *b, void (B::*mem) ());
+
+__attribute__ ((used)) void
+baz (B *b)
+{
+  bar (b, &B::abcabc);
+}
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf2_2.C b/gcc/testsuite/g++.dg/lto/devirtpmf2_2.C
new file mode 100644
index 00000000000..f7c4efa7fa4
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf2_2.C
@@ -0,0 +1,7 @@
+struct A { virtual void abcabc () = 0; };
+struct B : A { void abcabc () final; };
+
+static int cnt = 0;
+
+[[gnu::noinline]] void
+B::abcabc () { cnt++; };
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf3_0.C b/gcc/testsuite/g++.dg/lto/devirtpmf3_0.C
new file mode 100644
index 00000000000..15370b3188b
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf3_0.C
@@ -0,0 +1,13 @@
+/* { dg-lto-do link } */
+/* { dg-lto-options { "-O3 -flto -fdump-ipa-cp" } } */
+/* { dg-final { scan-wpa-ipa-dump "Discovered an indirect call to a known target.*defdef"  "cp"  } } */
+
+struct C;
+
+void
+bar (C *c, void (C::*mem) ())
+{
+  (c->*mem) ();
+}
+
+int main() {}
diff --git a/gcc/testsuite/g++.dg/lto/devirtpmf3_1.C b/gcc/testsuite/g++.dg/lto/devirtpmf3_1.C
new file mode 100644
index 00000000000..413c552fef7
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/devirtpmf3_1.C
@@ -0,0 +1,24 @@
+struct A { virtual void abcabc () final; };
+struct B { virtual void defdef () final; };
+struct C: A, B { virtual void mark () final; };
+
+static int cnt = 0;
+
+[[gnu::noinline]] void
+A::abcabc () { cnt++; };
+
+[[gnu::noinline]] void
+B::defdef () { cnt += 2; };
+
+// force emit vtable here
+void
+C::mark () { cnt += 3; };
+
+void
+bar (C *, void (C::*) ());
+
+__attribute__ ((used)) void
+baz (C *c)
+{
+  bar (c, &B::defdef);
+}
diff --git a/gcc/varpool.cc b/gcc/varpool.cc
index 1025535ec5a..8fc4d40c5d3 100644
--- a/gcc/varpool.cc
+++ b/gcc/varpool.cc
@@ -229,6 +229,8 @@ varpool_node::dump (FILE *f)
     fprintf (f, " write-only");
   if (tls_model)
     fprintf (f, " tls-%s", tls_model_names [tls_model]);
+  if (ptrmemfunc_vtable)
+    fprintf (f, " ptrmemfunc-vtable");
   fprintf (f, "\n");
 }
 
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.