diff --git a/gdb/finish-thread-state.h b/gdb/finish-thread-state.h
new file mode 100644
index 00000000000..60cd9be2b44
--- /dev/null
+++ b/gdb/finish-thread-state.h
@@ -0,0 +1,71 @@
+/* Copyright (C) 2025 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see . */
+
+#ifndef GDB_FINISH_THREAD_STATE_H
+#define GDB_FINISH_THREAD_STATE_H
+
+#include "gdbsupport/gdb-checked-static-cast.h"
+#include "gdbsupport/scope-exit.h"
+#include "gdbthread.h"
+#include "target.h"
+#include "process-stratum-target.h"
+
+namespace detail
+{
+
+/* Policy class for use with scope_exit_base. Calls finish_thread_state on
+ scope exit, unless release() is called to disengage. The release
+ mechanism is supplied by scope_exit_base. */
+
+struct scoped_finish_thread_state_policy
+{
+ /* Store TARG and PTID for a later call to finish_thread_state. For the
+ meaning of these arguments, see the comments on finish_thread_state. */
+ explicit scoped_finish_thread_state_policy (process_stratum_target *targ,
+ ptid_t ptid)
+ : m_ptid (ptid)
+ {
+ if (targ != nullptr)
+ m_target_ref = target_ops_ref::new_reference (targ);
+ }
+
+ /* Called on scope exit unless release was called, see scope_exit_base
+ for details. Calls finish_thread_state with stored target and ptid. */
+ void on_exit ()
+ {
+ target_ops *t = m_target_ref.get ();
+ process_stratum_target *p_target
+ = gdb::checked_static_cast (t);
+ finish_thread_state (p_target, m_ptid);
+ }
+
+private:
+
+ /* The process and target passed through to finish_thread_state. For
+ their use see the comments on that function. */
+ ptid_t m_ptid;
+ target_ops_ref m_target_ref;
+};
+
+}
+
+/* Calls finish_thread_state on scope exit, unless release() is called to
+ disengage. */
+using scoped_finish_thread_state
+ = scope_exit_base;
+
+#endif /* GDB_FINISH_THREAD_STATE_H */
diff --git a/gdb/gdbthread.h b/gdb/gdbthread.h
index 8ab5702d804..015da36ca71 100644
--- a/gdb/gdbthread.h
+++ b/gdb/gdbthread.h
@@ -859,11 +859,6 @@ extern bool threads_are_executing (process_stratum_target *targ);
Notifications are only emitted if the thread state did change. */
extern void finish_thread_state (process_stratum_target *targ, ptid_t ptid);
-/* Calls finish_thread_state on scope exit, unless release() is called
- to disengage. */
-using scoped_finish_thread_state
- = FORWARD_SCOPE_EXIT (finish_thread_state);
-
/* Commands with a prefix of `thread'. */
extern struct cmd_list_element *thread_cmd_list;
diff --git a/gdb/infcmd.c b/gdb/infcmd.c
index 5862b3e43ec..862cef6c1bb 100644
--- a/gdb/infcmd.c
+++ b/gdb/infcmd.c
@@ -57,6 +57,7 @@
#include "source.h"
#include "cli/cli-style.h"
#include "gdbsupport/selftest.h"
+#include "finish-thread-state.h"
/* Local functions: */
diff --git a/gdb/infrun.c b/gdb/infrun.c
index f1ce7195530..6bcd8ec5cc0 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -75,6 +75,7 @@
#include "extension.h"
#include "disasm.h"
#include "interps.h"
+#include "finish-thread-state.h"
/* Prototypes for local functions */
diff --git a/gdb/remote.c b/gdb/remote.c
index 8581d7d0a3b..1358e1c06b9 100644
--- a/gdb/remote.c
+++ b/gdb/remote.c
@@ -84,6 +84,7 @@
#include "cli/cli-style.h"
#include "gdbsupport/remote-args.h"
#include "gdbsupport/gdb_argv_vec.h"
+#include "finish-thread-state.h"
/* The remote target. */