Skip to content

unbox_value_or: Replace param::hstring with hstring#1558

Open
justanotheranonymoususer wants to merge 4 commits intomicrosoft:masterfrom
justanotheranonymoususer:unbox_value_or-hstring
Open

unbox_value_or: Replace param::hstring with hstring#1558
justanotheranonymoususer wants to merge 4 commits intomicrosoft:masterfrom
justanotheranonymoususer:unbox_value_or-hstring

Conversation

@justanotheranonymoususer
Copy link
Copy Markdown
Contributor

Partially fixes: #1527
Similar to #1530
@DefaultRyan please review

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 2, 2026

This is a performance regression, the following code which would previously use a fast-pass string (and be allocation-free) will now allocate a string on the heap:

auto foo = unbox_value_or<hstring>(nullptr, L"bar");

@justanotheranonymoususer
Copy link
Copy Markdown
Contributor Author

I don't think this is actually a regression. Let's compare the old and new code for unbox_value_or<hstring>(nullptr, L"bar"):

Old code (param::hstring):

  1. L"bar" > param::hstringWindowsCreateStringReference creates a fast-pass HSTRING on the stack
  2. nullptr fails the check, falls through to return *(hstring*)(&default_value)
  3. Return-by-value invokes hstring copy constructor > WindowsDuplicateString on the fast-pass handle > heap allocation (fast-pass strings can't be refcounted, so duplication always copies)

New code (D&&):

  1. L"bar" stays as const wchar_t(&)[4] --- no conversion
  2. nullptr fails the check, falls through to return hstring(std::forward<D>(default_value))
  3. hstring(L"bar") > WindowsCreateString > heap allocation

Both paths heap-allocate when the default is used. The function returns hstring by value, so there's no way to hand a fast-pass string to the caller (the stack-backed HSTRING_HEADER would be destroyed on return).

The new code is actually better on the success path: zero cost vs. a WindowsCreateStringReference + WindowsDeleteString round-trip that gets thrown away.

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 2, 2026

Return-by-value invokes hstring copy constructor

hstring has a move constructor that avoids calling WindowsDuplicateString, which would be used here, no?

@justanotheranonymoususer
Copy link
Copy Markdown
Contributor Author

*(hstring*)(&default_value) is an lvalue (a dereference expression). Implicit move on return only applies to id-expressions naming a local variable or parameter. So the copy constructor is called, not the move constructor.

return *(hstring*)(&default_value);  // lvalue > copy ctor > WindowsDuplicateString

If it were a named local, implicit move would kick in:

hstring result = ...;
return result;  // id-expression naming local > move ctor (no WindowsDuplicateString)

So the old code does copy-construct through WindowsDuplicateString, which promotes the fast-pass string to a heap-allocated one. Both old and new code allocate on the failure path.

Let me know if I'm missing something. Also intuitively, I can't see how const wchar_t(&)[4] > param::hstring > hstring can happen without allocation.

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 2, 2026

Hmmm yeah you're right, can't move out of a lvalue ref.

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.

Bug: winrt::box_value(std::wstring_view(...)) may cause abort

2 participants