Skip to content

Fix #339: ChaiScript assumes a non-const object reference is an R-value (&&)#685

Open
leftibot wants to merge 1 commit into
ChaiScript:developfrom
leftibot:fix/issue-339-chaiscript-assumes-a-non-const-object-re
Open

Fix #339: ChaiScript assumes a non-const object reference is an R-value (&&)#685
leftibot wants to merge 1 commit into
ChaiScript:developfrom
leftibot:fix/issue-339-chaiscript-assumes-a-non-const-object-re

Conversation

@leftibot
Copy link
Copy Markdown
Contributor

Automated fix by @leftibot.

What changed

Fix #339: Prevent implicit move from lvalue references during dispatch
Cast_Helper_Inner<T&&> unconditionally called std::move() on the underlying
object, even when the Boxed_Value held an lvalue reference (is_ref() == true).
This caused T&& overloads to win over const T& overloads for objects returned
by reference, silently moving from and destroying persistent objects. The fix
rejects the T&& cast for reference Boxed_Values, allowing dispatch to fall
through to the correct const T& overload.
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Files

 .../chaiscript/dispatchkit/boxed_cast_helper.hpp   |  3 +
 unittests/compiled_tests.cpp                       | 76 ++++++++++++++++++++++
 2 files changed, 79 insertions(+)

Closes #339

Triggered by @lefticus.

…ng dispatch

Cast_Helper_Inner<T&&> unconditionally called std::move() on the underlying
object, even when the Boxed_Value held an lvalue reference (is_ref() == true).
This caused T&& overloads to win over const T& overloads for objects returned
by reference, silently moving from and destroying persistent objects. The fix
rejects the T&& cast for reference Boxed_Values, allowing dispatch to fall
through to the correct const T& overload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lefticus
Copy link
Copy Markdown
Member

@leftibot should we add explicit support for && types and allow moves for them implicitly?

Sounds like potentially a lot of overhead for boxed_value but also seems necessary

@leftibot
Copy link
Copy Markdown
Contributor Author

Good question. I dug through the relevant code to understand the scope of what this would entail.

Current state

Right now, Boxed_Value has no concept of value category — it tracks m_is_ref (reference wrapper vs owned value) and m_return_value (used to decide whether to clone on assignment), but there's no m_is_rvalue flag. Similarly, Type_Info tracks is_reference but doesn't distinguish lvalue from rvalue references. The dispatch system uses bare_equal() which strips all qualifiers, so it cannot differentiate T& from T&& overloads.

The existing Cast_Helper_Inner<T&&> (line 122 of boxed_cast_helper.hpp) unconditionally calls std::move(), which is what #339 exposed as problematic for references.

What explicit && support would require

  1. Type_Info changes: Add an is_rvalue_reference_flag (bit 6 in m_flags), an is_rvalue_reference() query, and update Get_Type_Info<T> to detect std::is_rvalue_reference_v<T>. Minimal cost — one extra bit.

  2. Boxed_Value tracking: This is where the overhead concern is real. You'd need something like m_is_rvalue on the Data struct so the runtime knows "this value is safe to move from." The flag would be set for:

    • Temporaries / return values (already partially tracked via m_return_value)
    • Values explicitly marked movable by the script (e.g., move(x))

    The cost is one bool per Data allocation — negligible in memory, but the logic for when to set/clear it adds complexity.

  3. Dispatch changes: The overload scoring in dispatch() would need to prefer T&& overloads when the Boxed_Value is marked as rvalue, and reject them otherwise. This means bare_equal() alone isn't sufficient — you'd need value-category-aware scoring. Not massive, but it touches a hot path.

  4. Cast_Helper_Inner<T&&> guard: Should reject the cast (throw) when the Boxed_Value is not an rvalue, rather than the current approach of rejecting only references. This is essentially what the PR fix does, but generalized.

Recommendation

I'd suggest a phased approach:

Phase 1 (this PR): Keep the current fix — reject T&& casts for reference Boxed_Values. This prevents the destructive bug in #339 with minimal risk.

Phase 2 (follow-up): Leverage the existing m_return_value flag as the "safe to move" signal. return_value already means "this is a temporary" — so Cast_Helper_Inner<T&&> could allow the move only when is_return_value() == true or !is_ref(). This gets you most of the way without adding new state to Boxed_Value.

Phase 3 (if needed): Add Type_Info::is_rvalue_reference() and dispatch-level overload discrimination between T& and T&&. This is the full-featured version but requires careful thought about what rvalue semantics mean at the ChaiScript language level — should there be a move() built-in? Should function return values automatically prefer T&& overloads?

The overhead on Boxed_Value itself is minimal (one bool). The real cost is the complexity in dispatch and the semantic questions about what moves mean in a dynamically-typed scripting language. Phase 2 is probably the sweet spot — it gives correct behavior for the common cases (return values can be moved, named variables cannot) without needing language-level move semantics.

Want me to open an issue to track the phased approach?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChaiScript assumes a non-const object reference is an R-value (&&)

2 participants