Skip to content

several fixes to move semantics#7519

Open
Goober5000 wants to merge 5 commits into
scp-fs2open:masterfrom
Goober5000:move_fixes
Open

several fixes to move semantics#7519
Goober5000 wants to merge 5 commits into
scp-fs2open:masterfrom
Goober5000:move_fixes

Conversation

@Goober5000

Copy link
Copy Markdown
Contributor

Five commits, addressing various issues Claude found in an audit of the move semantics in the codebase:

  • harden move operations in w_bank and LuaThread
  • fix shallow ownership hazards in p_object, DecalDefinition, cmission, and Undo_stack
  • remove CombinedVariable code, which was never finished and is not called anywhere
  • store special_message in a unique_ptr, which also makes the training_message_queue struct safe for moves
  • fix geometry_batcher copy semantics and add move operations

@Goober5000 Goober5000 requested a review from z64555 as a code owner June 12, 2026 17:28
@Goober5000 Goober5000 added cleanup A modification or rewrite of code to make it more understandable or easier to maintain. fix A fix for bugs, not-a-bugs, and/or regressions. Waiting for Stable Marks a pull request that is to be merged after the next stable release, due to a release cycle labels Jun 12, 2026
@Goober5000 Goober5000 force-pushed the move_fixes branch 2 times, most recently from ea09071 to 12f2e38 Compare June 12, 2026 21:46
w_bank: add a move constructor.  object_copy_including_array_member
returns by value, and without a move constructor the return fell back
to the shallow copy constructor whenever NRVO was not applied (e.g.
MSVC debug builds), after which the local's destructor deleted the
freshly allocated arrays -- leaving the stored bank dangling and
double-deleting at model unload.  Also add noexcept to the move
operations, replace the destructor call in move-assignment with
explicit delete[]s (using an object after its lifetime has ended is
undefined behavior), guard against self-move, and document that the
defaulted copy constructor is intentionally shallow for
object_copy_including_array_member's copy-then-replace pattern.

LuaThread: declare the defaulted move operations noexcept.  This
exposed that LuaTable was not movable at all (its user-declared
destructor suppressed the implicit moves), so LuaThread's implicit
move spec was formally throwing -- a hard error on gcc 9, which
still requires the spec on a defaulted redeclaration to match the
implicit one.  Restore LuaTable's rule of five and declare the
LuaValue and LuaFunction copy operations noexcept (they only copy a
shared_ptr, a raw pointer, and an enum), making the noexcept claims
hold throughout the hierarchy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Goober5000 and others added 4 commits June 12, 2026 19:01
A sweep for the w_bank pattern (raw owning handles in types that are
copied, moved, or shifted by value) found four more instances:

p_object: the destructor frees dock_list, which suppresses the implicit
move operations, so Parse_objects growth copied elements -- safe only
because dock lists happen to be built after parsing finishes.  Wrap
dock_list in the new util::reset_on_move<> (a handle whose move resets
the source) and explicitly default the rule of five (with a user-defined
move assignment that frees any existing dock list before the memberwise
transfer), so moves now transfer the dock list.

DecalDefinition: the defaulted moves copied the bitmap handles without
resetting the source, so destroying a moved-from element would
bm_release live handles -- safe only because DecalDefinitions grows
before loadBitmaps runs.  Wrap the handles in reset_on_move<int, -1>.

cmission: FRED's remove_mission overwrote the removed slot's five
vm_strdup'd strings without freeing them (a leak on every removal) and
left the vacated slot aliasing the surviving copy (a double-free if the
slot is later reused without reassigning every string).  Factor the
string cleanup out of mission_campaign_clear into
mission_campaign_free_mission_strings and call it before the overwrite,
nulling the vacated slot afterward.

Undo_stack: the destructor deletes the tracked items, but the implicit
shallow copy was used to transfer stacks into the undo system, kept safe
only by an immediately-following untrack() call.  Delete the copy
operations, default the move constructor and implement move assignment
as clear-then-exchange, so an assigned-over stack deletes its own items
first and the source is guaranteed empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
clone() allocated fresh vert/radius_list buffers without freeing the
ones already owned, so assigning over a live batcher leaked both.  It
also never copied buffer_offset, and the copy constructor called it
with no member initialization at all -- so every copy-constructed
batcher carried an uninitialized buffer_offset.  Free the existing
buffers in clone(), copy buffer_offset, and delegate the copy
constructor to the default constructor so clone() sees initialized
members (which is also what makes its new frees safe on that path).

Also add noexcept move operations, which the user-declared copy
operations had suppressed, so by-value handling can transfer the
buffers instead of deep-copying them.

None of these defects were reachable from current callers (the static
batch maps mutate entries in place); this closes the latent hazards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup A modification or rewrite of code to make it more understandable or easier to maintain. fix A fix for bugs, not-a-bugs, and/or regressions. Waiting for Stable Marks a pull request that is to be merged after the next stable release, due to a release cycle

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant