From 97047371b627cb4d2fdc98dbeb36480fd2b817be Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 15:11:38 -0700 Subject: [PATCH 1/8] [Copilot] Add wil::make_ready and wil::batched cppwinrt helpers Implements the two helpers spun out of microsoft/cppwinrt#1608 into WIL: - make_ready() / make_ready(value) / make_failed(): already-settled IAsyncAction / IAsyncOperation with no coroutine frame, firing Completed inline with a single-assignment guard (#663). - batched(collection): range-for adapter that prefetches elements in blocks via GetMany instead of one ABI crossing per element, for indexed (IVector/IVectorView) and iterable-only (IIterable/IIterator, including map IKeyValuePair) collections (#664). Both live in cppwinrt_helpers.h next to to_vector, reusing its is_winrt_vector_like / is_winrt_iterator_like detection and the re-includable per-header guard pattern. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 406 +++++++++++++++++++++++++++++++++ tests/CppWinRTTests.cpp | 175 +++++++++++++- 2 files changed, 580 insertions(+), 1 deletion(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index d3c09341a..d4171b265 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -288,6 +288,210 @@ namespace details return {}; } } + + // Number of elements to prefetch per GetMany call. Aim for ~2KB blocks, clamped to [1, 128]. + constexpr uint32_t batched_block_size(size_t element_size) noexcept + { + size_t count = element_size ? (size_t{2048} / element_size) : size_t{128}; + if (count < 1) + { + count = 1; + } + if (count > 128) + { + count = 128; + } + return static_cast(count); + } + + // Forward iterator over an indexed (GetAt-capable) collection that prefetches a block per + // GetMany call and serves sequential reads from it, so range-for crosses the ABI ~once per + // block instead of once per element. + template + struct batched_indexed_iterator + { + using value_type = decltype(std::declval().GetAt(0)); + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using pointer = value_type const*; + using reference = value_type const&; + + static constexpr uint32_t buffer_capacity = batched_block_size(sizeof(value_type)); + + batched_indexed_iterator() = default; + + batched_indexed_iterator(Collection collection, uint32_t index) : + m_collection(std::move(collection)), m_index(index) + { + } + + reference operator*() const + { + return fetch(m_index); + } + + pointer operator->() const + { + return std::addressof(fetch(m_index)); + } + + batched_indexed_iterator& operator++() + { + ++m_index; + return *this; + } + + batched_indexed_iterator operator++(int) + { + auto previous = *this; + ++m_index; + return previous; + } + + bool operator==(batched_indexed_iterator const& other) const noexcept + { + return m_index == other.m_index; + } + + bool operator!=(batched_indexed_iterator const& other) const noexcept + { + return m_index != other.m_index; + } + + private: + reference fetch(uint32_t index) const + { + if ((index < m_buffer_base) || (index >= m_buffer_base + m_buffer_size)) + { + m_buffer_base = index; + m_buffer_size = m_collection.GetMany(index, m_buffer); + } + + if (index < m_buffer_base + m_buffer_size) + { + return m_buffer[index - m_buffer_base]; + } + + // Past the end of what GetMany returned; defer to GetAt so component bounds behavior wins. + m_fallback = m_collection.GetAt(index); + return m_fallback; + } + + Collection m_collection{nullptr}; + uint32_t m_index{0}; + mutable uint32_t m_buffer_base{0}; + mutable uint32_t m_buffer_size{0}; + mutable value_type m_fallback{empty()}; + mutable std::array m_buffer{}; + }; + + template + struct batched_indexed_range + { + explicit batched_indexed_range(Collection collection) : + m_collection(std::move(collection)), m_size(m_collection.Size()) + { + } + + batched_indexed_iterator begin() const + { + return {m_collection, 0}; + } + + batched_indexed_iterator end() const + { + return {m_collection, m_size}; + } + + private: + Collection m_collection; + uint32_t m_size{0}; + }; + + // Single-pass forward iterator that batches an IIterator via GetMany into a small buffer and + // yields from it, so range-for over a collection that lacks GetAt crosses the ABI once per + // block instead of once per element (Current/MoveNext). + template + struct batched_buffered_iterator + { + using value_type = decltype(std::declval().Current()); + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using pointer = value_type const*; + using reference = value_type const&; + + static constexpr uint32_t buffer_capacity = batched_block_size(sizeof(value_type)); + + batched_buffered_iterator() = default; + + explicit batched_buffered_iterator(Iterator iterator) : m_iterator(std::move(iterator)) + { + fill(); + } + + reference operator*() const noexcept + { + return m_buffer[m_index]; + } + + pointer operator->() const noexcept + { + return std::addressof(m_buffer[m_index]); + } + + batched_buffered_iterator& operator++() + { + if (++m_index == m_size) + { + fill(); + } + + return *this; + } + + bool operator==(batched_buffered_iterator const& other) const noexcept + { + return (m_size == 0) && (other.m_size == 0); + } + + bool operator!=(batched_buffered_iterator const& other) const noexcept + { + return !(*this == other); + } + + private: + void fill() + { + m_index = 0; + m_size = m_iterator ? m_iterator.GetMany(m_buffer) : 0; + } + + Iterator m_iterator{nullptr}; + std::array m_buffer{}; + uint32_t m_size{0}; + uint32_t m_index{0}; + }; + + template + struct batched_iterable_range + { + explicit batched_iterable_range(Iterator iterator) : m_iterator(std::move(iterator)) + { + } + + batched_buffered_iterator begin() + { + return batched_buffered_iterator{std::move(m_iterator)}; + } + + batched_buffered_iterator end() const noexcept + { + return {}; + } + + private: + Iterator m_iterator; + }; } // namespace details /// @endcond @@ -349,9 +553,211 @@ auto to_vector(TSrc const& src) return to_vector(src.First()); } } + +/** Adapts a C++/WinRT collection for range-for so that elements are prefetched in blocks via +GetMany instead of one ABI round-trip per element. On a cross-process or heavily-marshaled +collection the per-element crossings are the dominant cost, so batching them cuts that cost to +roughly one crossing per block. +@code +winrt::IVector collection = GetCollection(); +for (winrt::hstring const& item : wil::batched(collection)) +{ + // use item +} +@endcode +Works for IVector, IVectorView, IIterable, IIterator, and any type or interface that +C++/WinRT projects those interfaces for (PropertySet, IMap, etc.). Indexed collections +(those exposing GetAt) prefetch blocks with GetMany(index, ...) while preserving the component's +end-of-range behavior; iterable-only collections buffer through IIterator::GetMany. + +The traversal is single-pass and buffering: a yielded element outlives the step that produced it, +matching the observable behavior of wil::to_vector(collection). The returned range and its +iterators keep the collection alive for the duration of the loop. +*/ +template +auto batched(TSrc src) +{ + if constexpr (details::is_winrt_vector_like::value) + { + return details::batched_indexed_range{std::move(src)}; + } + else if constexpr (details::is_winrt_iterator_like::value) + { + return details::batched_iterable_range{std::move(src)}; + } + else + { + using Iterator = decltype(src.First()); + return details::batched_iterable_range{src.First()}; + } +} } // namespace wil #endif +#if (defined(WINRT_Windows_Foundation_H) && !defined(__WIL_CPPWINRT_WINDOWS_FOUNDATION_HELPERS)) || defined(WIL_DOXYGEN) +#define __WIL_CPPWINRT_WINDOWS_FOUNDATION_HELPERS +namespace wil +{ +/// @cond +namespace details +{ + // Shared base for a ready-made, already-settled async object. It fires the Completed handler + // inline (no coroutine frame, no mutex) and exposes the IAsyncInfo surface. A settled object is + // either Completed (m_error == S_OK) or Error (m_error is a failure HRESULT). + template + struct ready_async_base : winrt::implements + { + ready_async_base() = default; + + explicit ready_async_base(winrt::hresult error) noexcept : m_error(error) + { + } + + void Completed(CompletedHandler const& handler) + { + // Match the coroutine promise contract: Completed may be assigned at most once. + if (std::exchange(m_completed_assigned, true)) + { + throw winrt::hresult_illegal_delegate_assignment(); + } + + if (handler) + { + handler(static_cast(this)->get_strong().template as(), Status()); + } + } + + CompletedHandler Completed() const noexcept + { + return {nullptr}; + } + + uint32_t Id() const noexcept + { + return 1; + } + + winrt::Windows::Foundation::AsyncStatus Status() const noexcept + { + return (m_error < 0) ? winrt::Windows::Foundation::AsyncStatus::Error + : winrt::Windows::Foundation::AsyncStatus::Completed; + } + + winrt::hresult ErrorCode() const noexcept + { + return m_error; + } + + void Cancel() const noexcept + { + } + + void Close() const noexcept + { + } + + protected: + winrt::hresult m_error{}; + + private: + bool m_completed_assigned{false}; + }; + + template + struct ready_async_operation : + ready_async_base< + ready_async_operation, + winrt::Windows::Foundation::IAsyncOperation, + winrt::Windows::Foundation::AsyncOperationCompletedHandler> + { + using base = ready_async_base< + ready_async_operation, + winrt::Windows::Foundation::IAsyncOperation, + winrt::Windows::Foundation::AsyncOperationCompletedHandler>; + + explicit ready_async_operation(TResult value) : m_result(std::move(value)) + { + } + + explicit ready_async_operation(winrt::hresult error) : base(error) + { + } + + TResult GetResults() + { + winrt::check_hresult(this->m_error); + return m_result; + } + + private: + TResult m_result{}; + }; + + struct ready_async_action : + ready_async_base< + ready_async_action, + winrt::Windows::Foundation::IAsyncAction, + winrt::Windows::Foundation::AsyncActionCompletedHandler> + { + using base = ready_async_base< + ready_async_action, + winrt::Windows::Foundation::IAsyncAction, + winrt::Windows::Foundation::AsyncActionCompletedHandler>; + + ready_async_action() = default; + + explicit ready_async_action(winrt::hresult error) : base(error) + { + } + + void GetResults() + { + winrt::check_hresult(this->m_error); + } + }; +} // namespace details +/// @endcond + +/** Returns an IAsyncOperation already in the Completed state carrying @p value, with no +coroutine frame. `co_await`, `.get()`, and a `Completed` handler all complete synchronously. +@code +winrt::Windows::Foundation::IAsyncOperation GetCachedValue() +{ + if (m_haveValue) + { + return wil::make_ready(m_value); // no coroutine frame for the already-known answer + } + return ComputeValueAsync(); +} +@endcode +*/ +template +winrt::Windows::Foundation::IAsyncOperation> make_ready(TResult&& value) +{ + return winrt::make>>(std::forward(value)); +} + +//! Returns an IAsyncAction already in the Completed state, with no coroutine frame. +inline winrt::Windows::Foundation::IAsyncAction make_ready() +{ + return winrt::make(); +} + +//! Returns an IAsyncAction already in the Error state carrying @p error; GetResults() throws it. +inline winrt::Windows::Foundation::IAsyncAction make_failed(winrt::hresult error) +{ + return winrt::make(error); +} + +//! Returns an IAsyncOperation already in the Error state carrying @p error; GetResults() throws it. +template +winrt::Windows::Foundation::IAsyncOperation make_failed(winrt::hresult error) +{ + return winrt::make>(error); +} +} // namespace wil +#endif // __WIL_CPPWINRT_WINDOWS_FOUNDATION_HELPERS + #if (defined(WINRT_Windows_UI_H) && defined(_WINDOWS_UI_INTEROP_H_) && !defined(__WIL_CPPWINRT_WINDOWS_UI_INTEROP_HELPERS)) || \ defined(WIL_DOXYGEN) /// @cond diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index 0db4e4916..9b2daa1d9 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -775,4 +775,177 @@ TEST_CASE("CppWinRTTests::ZStringViewFromHString", "[cppwinrt]") { winrt::hstring hstr = L"Hello"; REQUIRE(wil::zwstring_view(hstr) == hstr); -} \ No newline at end of file +} +TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") +{ + using namespace winrt::Windows::Foundation::Collections; + + // Indexed collection spanning multiple GetMany blocks (int32 block is 128). + { + std::vector expected; + for (int32_t i = 0; i < 300; ++i) + { + expected.push_back(i); + } + + auto vec = winrt::single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : wil::batched(vec)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + + // The read-only view goes through the same indexed path. + observed.clear(); + for (auto&& value : wil::batched(vec.GetView())) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Exactly one element beyond a single block boundary. + { + auto vec = winrt::single_threaded_vector(std::vector(129, 7)); + uint32_t count = 0; + for (auto&& value : wil::batched(vec)) + { + REQUIRE(value == 7); + ++count; + } + REQUIRE(count == 129); + } + + // Empty collection yields nothing. + { + auto vec = winrt::single_threaded_vector(); + uint32_t count = 0; + for (auto&& value : wil::batched(vec)) + { + (void)value; + ++count; + } + REQUIRE(count == 0); + } + + // Iterable-only path (IIterable has no GetAt) buffers through IIterator::GetMany. + { + std::vector expected = {L"a", L"b", L"c"}; + IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : wil::batched(iterable)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Directly batching an iterator yields its current position onward. + { + auto vec = winrt::single_threaded_vector({1, 2, 3, 4, 5}); + std::vector observed; + for (auto&& value : wil::batched(vec.First())) + { + observed.push_back(value); + } + REQUIRE(observed == std::vector({1, 2, 3, 4, 5})); + } + + // Map batches over IKeyValuePair through the iterable path. + { + std::map src{{L"kittens", L"fluffy"}, {L"puppies", L"cute"}}; + auto map = winrt::single_threaded_map(std::map(src)); + uint32_t count = 0; + for (auto&& pair : wil::batched(map)) + { + REQUIRE(pair.Value() == src.at(pair.Key())); + ++count; + } + REQUIRE(count == src.size()); + } + + // Non-WinRT indexed shape works too, matching to_vector's duck typing. + { + uint32_t count = 0; + for (auto&& value : wil::batched(vector_like{})) + { + REQUIRE(value == vector_like{}.GetAt(0)); + ++count; + } + REQUIRE(count == vector_like{}.Size()); + } +} + +TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") +{ + using namespace winrt; + using namespace winrt::Windows::Foundation; + + // Completed synchronously with a value, with no coroutine frame. + { + IAsyncOperation op = wil::make_ready(42); + REQUIRE(op.Status() == AsyncStatus::Completed); + REQUIRE(op.ErrorCode() == 0); + REQUIRE(op.GetResults() == 42); + REQUIRE(op.get() == 42); + } + + // co_await yields the value through the synchronous-completion path. + { + auto coro = []() -> IAsyncOperation { + co_return co_await wil::make_ready(7); + }; + REQUIRE(coro().get() == 7); + } + + // A Completed handler on an already-completed operation fires immediately. + { + auto op = wil::make_ready(5); + int32_t observed = 0; + AsyncStatus observed_status = AsyncStatus::Started; + op.Completed([&](IAsyncOperation const& sender, AsyncStatus status) { + observed = sender.GetResults(); + observed_status = status; + }); + REQUIRE(observed == 5); + REQUIRE(observed_status == AsyncStatus::Completed); + } + + // Assigning Completed twice is illegal, matching the coroutine promise. + { + auto op = wil::make_ready(1); + op.Completed([](auto&&, auto&&) {}); + REQUIRE_THROWS_AS(op.Completed([](auto&&, auto&&) {}), hresult_illegal_delegate_assignment); + } + + // Action variant carries no result. + { + IAsyncAction action = wil::make_ready(); + REQUIRE(action.Status() == AsyncStatus::Completed); + action.get(); + } + + // A non-trivial result type round-trips. + { + auto op = wil::make_ready(hstring{L"ready"}); + REQUIRE(op.get() == L"ready"); + } + + // Failed action: Error status, GetResults/get throw the carried HRESULT. + { + IAsyncAction action = wil::make_failed(E_ACCESSDENIED); + REQUIRE(action.Status() == AsyncStatus::Error); + REQUIRE(action.ErrorCode() == E_ACCESSDENIED); + REQUIRE_THROWS_AS(action.get(), hresult_access_denied); + } + + // Failed operation: GetResults throws the carried HRESULT. + { + auto op = wil::make_failed(E_INVALIDARG); + REQUIRE(op.Status() == AsyncStatus::Error); + REQUIRE_THROWS_AS(op.GetResults(), hresult_invalid_argument); + } +} From 04fe30fe90afd0e8587bd5f2a387fabce7b1a2eb Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 15:24:43 -0700 Subject: [PATCH 2/8] Use std::clamp in batched_block_size Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index d4171b265..610121dd8 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -292,16 +292,7 @@ namespace details // Number of elements to prefetch per GetMany call. Aim for ~2KB blocks, clamped to [1, 128]. constexpr uint32_t batched_block_size(size_t element_size) noexcept { - size_t count = element_size ? (size_t{2048} / element_size) : size_t{128}; - if (count < 1) - { - count = 1; - } - if (count > 128) - { - count = 128; - } - return static_cast(count); + return static_cast(std::clamp(size_t{2048} / (element_size ? element_size : 1), 1, 128)); } // Forward iterator over an indexed (GetAt-capable) collection that prefetches a block per From cb0a7ad66af0e6f80d529f3a99fdc78243c32a8e Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 15:50:00 -0700 Subject: [PATCH 3/8] Unify batched iterators into one input iterator + refill source Collapse batched_indexed_iterator and batched_buffered_iterator into a single input iterator parameterized on a small refill 'source' policy. As a range-for-only adapter it no longer needs Size(), the GetAt fallback, or random access: it block-prefetches via GetMany and stops when a block comes back short, exactly matching to_vector's exhaustion rule. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 165 +++++++++++---------------------- 1 file changed, 52 insertions(+), 113 deletions(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index 610121dd8..16ce1c999 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -295,117 +295,48 @@ namespace details return static_cast(std::clamp(size_t{2048} / (element_size ? element_size : 1), 1, 128)); } - // Forward iterator over an indexed (GetAt-capable) collection that prefetches a block per - // GetMany call and serves sequential reads from it, so range-for crosses the ABI ~once per - // block instead of once per element. - template - struct batched_indexed_iterator + // Refills a block from an IIterator via GetMany. The iterator carries its own cursor, so each + // call just pulls the next run of elements; a short (or empty) block means exhaustion. + template + struct batched_iterator_source { - using value_type = decltype(std::declval().GetAt(0)); - using iterator_category = std::input_iterator_tag; - using difference_type = std::ptrdiff_t; - using pointer = value_type const*; - using reference = value_type const&; - - static constexpr uint32_t buffer_capacity = batched_block_size(sizeof(value_type)); - - batched_indexed_iterator() = default; - - batched_indexed_iterator(Collection collection, uint32_t index) : - m_collection(std::move(collection)), m_index(index) - { - } - - reference operator*() const - { - return fetch(m_index); - } - - pointer operator->() const - { - return std::addressof(fetch(m_index)); - } - - batched_indexed_iterator& operator++() - { - ++m_index; - return *this; - } - - batched_indexed_iterator operator++(int) - { - auto previous = *this; - ++m_index; - return previous; - } - - bool operator==(batched_indexed_iterator const& other) const noexcept - { - return m_index == other.m_index; - } + using value_type = decltype(std::declval().Current()); - bool operator!=(batched_indexed_iterator const& other) const noexcept + uint32_t fill(winrt::array_view block) { - return m_index != other.m_index; - } - - private: - reference fetch(uint32_t index) const - { - if ((index < m_buffer_base) || (index >= m_buffer_base + m_buffer_size)) - { - m_buffer_base = index; - m_buffer_size = m_collection.GetMany(index, m_buffer); - } - - if (index < m_buffer_base + m_buffer_size) - { - return m_buffer[index - m_buffer_base]; - } - - // Past the end of what GetMany returned; defer to GetAt so component bounds behavior wins. - m_fallback = m_collection.GetAt(index); - return m_fallback; + return m_iterator.GetMany(block); } - Collection m_collection{nullptr}; - uint32_t m_index{0}; - mutable uint32_t m_buffer_base{0}; - mutable uint32_t m_buffer_size{0}; - mutable value_type m_fallback{empty()}; - mutable std::array m_buffer{}; + Iterator m_iterator{}; }; + // Refills a block from an indexed (GetAt-capable) collection via GetMany, tracking the running + // start index. A short (or empty) block means exhaustion, so no end-index or Size() is needed. template - struct batched_indexed_range + struct batched_indexed_source { - explicit batched_indexed_range(Collection collection) : - m_collection(std::move(collection)), m_size(m_collection.Size()) - { - } - - batched_indexed_iterator begin() const - { - return {m_collection, 0}; - } + using value_type = decltype(std::declval().GetAt(0)); - batched_indexed_iterator end() const + uint32_t fill(winrt::array_view block) { - return {m_collection, m_size}; + uint32_t const fetched = m_collection.GetMany(m_start, block); + m_start += fetched; + return fetched; } - private: - Collection m_collection; - uint32_t m_size{0}; + Collection m_collection{}; + uint32_t m_start{0}; }; - // Single-pass forward iterator that batches an IIterator via GetMany into a small buffer and - // yields from it, so range-for over a collection that lacks GetAt crosses the ABI once per - // block instead of once per element (Current/MoveNext). - template - struct batched_buffered_iterator + // Single-pass input iterator that batches a source via GetMany into a small buffer and yields + // from it, so range-for crosses the ABI once per block instead of once per element. The two + // shapes (IIterator vs indexed collection) differ only in how a block is refilled, which the + // Source policy supplies. A block shorter than the buffer signals the last block; a full block + // is followed by one more refill (which returns empty at the boundary), matching to_vector. + template + struct batched_iterator { - using value_type = decltype(std::declval().Current()); + using value_type = typename Source::value_type; using iterator_category = std::input_iterator_tag; using difference_type = std::ptrdiff_t; using pointer = value_type const*; @@ -413,9 +344,9 @@ namespace details static constexpr uint32_t buffer_capacity = batched_block_size(sizeof(value_type)); - batched_buffered_iterator() = default; + batched_iterator() = default; // default-constructed is the end sentinel (m_size == 0) - explicit batched_buffered_iterator(Iterator iterator) : m_iterator(std::move(iterator)) + explicit batched_iterator(Source source) : m_source(std::move(source)) { fill(); } @@ -430,22 +361,30 @@ namespace details return std::addressof(m_buffer[m_index]); } - batched_buffered_iterator& operator++() + batched_iterator& operator++() { if (++m_index == m_size) { - fill(); + // A full block might have more behind it; a short block was the last one. + if (m_size == buffer_capacity) + { + fill(); + } + else + { + m_size = 0; + } } return *this; } - bool operator==(batched_buffered_iterator const& other) const noexcept + bool operator==(batched_iterator const& other) const noexcept { return (m_size == 0) && (other.m_size == 0); } - bool operator!=(batched_buffered_iterator const& other) const noexcept + bool operator!=(batched_iterator const& other) const noexcept { return !(*this == other); } @@ -454,34 +393,34 @@ namespace details void fill() { m_index = 0; - m_size = m_iterator ? m_iterator.GetMany(m_buffer) : 0; + m_size = m_source.fill(m_buffer); } - Iterator m_iterator{nullptr}; + Source m_source{}; std::array m_buffer{}; uint32_t m_size{0}; uint32_t m_index{0}; }; - template - struct batched_iterable_range + template + struct batched_range { - explicit batched_iterable_range(Iterator iterator) : m_iterator(std::move(iterator)) + explicit batched_range(Source source) : m_source(std::move(source)) { } - batched_buffered_iterator begin() + batched_iterator begin() { - return batched_buffered_iterator{std::move(m_iterator)}; + return batched_iterator{std::move(m_source)}; } - batched_buffered_iterator end() const noexcept + batched_iterator end() const noexcept { return {}; } private: - Iterator m_iterator; + Source m_source; }; } // namespace details /// @endcond @@ -570,16 +509,16 @@ auto batched(TSrc src) { if constexpr (details::is_winrt_vector_like::value) { - return details::batched_indexed_range{std::move(src)}; + return details::batched_range>{{std::move(src), 0}}; } else if constexpr (details::is_winrt_iterator_like::value) { - return details::batched_iterable_range{std::move(src)}; + return details::batched_range>{{std::move(src)}}; } else { using Iterator = decltype(src.First()); - return details::batched_iterable_range{src.First()}; + return details::batched_range>{{src.First()}}; } } } // namespace wil From 17480cc68022469f7d8479c5688eab2bcc1a6fda Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 17:06:19 -0700 Subject: [PATCH 4/8] Rename wil::batched to wil::batched_range Rename the public helper and its detail range struct (batched_range -> batched_view) to avoid a name clash. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 20 ++++++++++---------- tests/CppWinRTTests.cpp | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index 16ce1c999..e6c3447b1 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -403,9 +403,9 @@ namespace details }; template - struct batched_range + struct batched_view { - explicit batched_range(Source source) : m_source(std::move(source)) + explicit batched_view(Source source) : m_source(std::move(source)) { } @@ -490,35 +490,35 @@ collection the per-element crossings are the dominant cost, so batching them cut roughly one crossing per block. @code winrt::IVector collection = GetCollection(); -for (winrt::hstring const& item : wil::batched(collection)) +for (winrt::hstring const& item : wil::batched_range(collection)) { // use item } @endcode Works for IVector, IVectorView, IIterable, IIterator, and any type or interface that -C++/WinRT projects those interfaces for (PropertySet, IMap, etc.). Indexed collections -(those exposing GetAt) prefetch blocks with GetMany(index, ...) while preserving the component's -end-of-range behavior; iterable-only collections buffer through IIterator::GetMany. +C++/WinRT projects those interfaces for (PropertySet, IMap, etc.). Indexed collections (those +exposing GetAt) prefetch blocks with GetMany(index, ...); iterable-only collections buffer through +IIterator::GetMany. Either way the block prefetch stops once GetMany returns a short block. The traversal is single-pass and buffering: a yielded element outlives the step that produced it, matching the observable behavior of wil::to_vector(collection). The returned range and its iterators keep the collection alive for the duration of the loop. */ template -auto batched(TSrc src) +auto batched_range(TSrc src) { if constexpr (details::is_winrt_vector_like::value) { - return details::batched_range>{{std::move(src), 0}}; + return details::batched_view>{{std::move(src), 0}}; } else if constexpr (details::is_winrt_iterator_like::value) { - return details::batched_range>{{std::move(src)}}; + return details::batched_view>{{std::move(src)}}; } else { using Iterator = decltype(src.First()); - return details::batched_range>{{src.First()}}; + return details::batched_view>{{src.First()}}; } } } // namespace wil diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index 9b2daa1d9..a44d75b85 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -791,7 +791,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") auto vec = winrt::single_threaded_vector(std::vector(expected)); std::vector observed; - for (auto&& value : wil::batched(vec)) + for (auto&& value : wil::batched_range(vec)) { observed.push_back(value); } @@ -799,7 +799,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") // The read-only view goes through the same indexed path. observed.clear(); - for (auto&& value : wil::batched(vec.GetView())) + for (auto&& value : wil::batched_range(vec.GetView())) { observed.push_back(value); } @@ -810,7 +810,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") { auto vec = winrt::single_threaded_vector(std::vector(129, 7)); uint32_t count = 0; - for (auto&& value : wil::batched(vec)) + for (auto&& value : wil::batched_range(vec)) { REQUIRE(value == 7); ++count; @@ -822,7 +822,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") { auto vec = winrt::single_threaded_vector(); uint32_t count = 0; - for (auto&& value : wil::batched(vec)) + for (auto&& value : wil::batched_range(vec)) { (void)value; ++count; @@ -836,7 +836,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); std::vector observed; - for (auto&& value : wil::batched(iterable)) + for (auto&& value : wil::batched_range(iterable)) { observed.push_back(value); } @@ -847,7 +847,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") { auto vec = winrt::single_threaded_vector({1, 2, 3, 4, 5}); std::vector observed; - for (auto&& value : wil::batched(vec.First())) + for (auto&& value : wil::batched_range(vec.First())) { observed.push_back(value); } @@ -859,7 +859,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") std::map src{{L"kittens", L"fluffy"}, {L"puppies", L"cute"}}; auto map = winrt::single_threaded_map(std::map(src)); uint32_t count = 0; - for (auto&& pair : wil::batched(map)) + for (auto&& pair : wil::batched_range(map)) { REQUIRE(pair.Value() == src.at(pair.Key())); ++count; @@ -870,7 +870,7 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") // Non-WinRT indexed shape works too, matching to_vector's duck typing. { uint32_t count = 0; - for (auto&& value : wil::batched(vector_like{})) + for (auto&& value : wil::batched_range(vector_like{})) { REQUIRE(value == vector_like{}.GetAt(0)); ++count; From 6f13c5f5613833062e95dc64536fa4c300902d53 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 17:15:06 -0700 Subject: [PATCH 5/8] Add batched_range boundary and mid-stream iterator tests Cover exact block-boundary multiples (1/127/128/129/256/257) on both the indexed and iterable paths to exercise the full-block-then-empty-refill termination, ordering across seams, single element, and an IIterator advanced past its start yielding only the remainder. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/CppWinRTTests.cpp | 66 ++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index a44d75b85..a76ec3047 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -867,16 +867,62 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") REQUIRE(count == src.size()); } - // Non-WinRT indexed shape works too, matching to_vector's duck typing. - { - uint32_t count = 0; - for (auto&& value : wil::batched_range(vector_like{})) - { - REQUIRE(value == vector_like{}.GetAt(0)); - ++count; - } - REQUIRE(count == vector_like{}.Size()); - } + // Non-WinRT indexed shape works too, matching to_vector's duck typing. + { + uint32_t count = 0; + for (auto&& value : wil::batched_range(vector_like{})) + { + REQUIRE(value == vector_like{}.GetAt(0)); + ++count; + } + REQUIRE(count == vector_like{}.Size()); + } + + // Block-boundary edge cases. For int32 the prefetch block is 128, so exercise exactly one and + // exactly two full blocks: the "full block implies maybe-more" rule must fetch the trailing + // empty block and terminate cleanly -- no infinite loop, no dropped or duplicated element. + for (int32_t total : {1, 127, 128, 129, 256, 257}) + { + std::vector expected; + for (int32_t i = 0; i < total; ++i) + { + expected.push_back(i); + } + + // Indexed path. + auto vec = winrt::single_threaded_vector(std::vector(expected)); + std::vector observed; + for (auto&& value : wil::batched_range(vec)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); // exact count and in-order, so no skip/dup across seams + + // Iterable-only path exercises the same boundary through IIterator::GetMany. + IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); + observed.clear(); + for (auto&& value : wil::batched_range(iterable)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Batching an iterator already advanced past its start yields only the remainder, matching + // to_vector's "current position and everything after it" contract (no re-anchor to index 0). + { + auto vec = winrt::single_threaded_vector({10, 20, 30, 40}); + auto it = vec.First(); + REQUIRE(it.Current() == 10); + it.MoveNext(); // now positioned at 20 + + std::vector observed; + for (auto&& value : wil::batched_range(it)) + { + observed.push_back(value); + } + REQUIRE(observed == std::vector({20, 30, 40})); + } } TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") From b4672ab46cf449eb88696cc8bc893f65e46f40d1 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 17:21:47 -0700 Subject: [PATCH 6/8] Rename make_ready/make_failed to already_complete/already_failed Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 10 +++++----- tests/CppWinRTTests.cpp | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index e6c3447b1..1e5d9afc6 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -655,33 +655,33 @@ winrt::Windows::Foundation::IAsyncOperation GetCachedValue() { if (m_haveValue) { - return wil::make_ready(m_value); // no coroutine frame for the already-known answer + return wil::already_complete(m_value); // no coroutine frame for the already-known answer } return ComputeValueAsync(); } @endcode */ template -winrt::Windows::Foundation::IAsyncOperation> make_ready(TResult&& value) +winrt::Windows::Foundation::IAsyncOperation> already_complete(TResult&& value) { return winrt::make>>(std::forward(value)); } //! Returns an IAsyncAction already in the Completed state, with no coroutine frame. -inline winrt::Windows::Foundation::IAsyncAction make_ready() +inline winrt::Windows::Foundation::IAsyncAction already_complete() { return winrt::make(); } //! Returns an IAsyncAction already in the Error state carrying @p error; GetResults() throws it. -inline winrt::Windows::Foundation::IAsyncAction make_failed(winrt::hresult error) +inline winrt::Windows::Foundation::IAsyncAction already_failed(winrt::hresult error) { return winrt::make(error); } //! Returns an IAsyncOperation already in the Error state carrying @p error; GetResults() throws it. template -winrt::Windows::Foundation::IAsyncOperation make_failed(winrt::hresult error) +winrt::Windows::Foundation::IAsyncOperation already_failed(winrt::hresult error) { return winrt::make>(error); } diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index a76ec3047..2f93f8058 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -932,7 +932,7 @@ TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") // Completed synchronously with a value, with no coroutine frame. { - IAsyncOperation op = wil::make_ready(42); + IAsyncOperation op = wil::already_complete(42); REQUIRE(op.Status() == AsyncStatus::Completed); REQUIRE(op.ErrorCode() == 0); REQUIRE(op.GetResults() == 42); @@ -942,14 +942,14 @@ TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") // co_await yields the value through the synchronous-completion path. { auto coro = []() -> IAsyncOperation { - co_return co_await wil::make_ready(7); + co_return co_await wil::already_complete(7); }; REQUIRE(coro().get() == 7); } // A Completed handler on an already-completed operation fires immediately. { - auto op = wil::make_ready(5); + auto op = wil::already_complete(5); int32_t observed = 0; AsyncStatus observed_status = AsyncStatus::Started; op.Completed([&](IAsyncOperation const& sender, AsyncStatus status) { @@ -962,27 +962,27 @@ TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") // Assigning Completed twice is illegal, matching the coroutine promise. { - auto op = wil::make_ready(1); + auto op = wil::already_complete(1); op.Completed([](auto&&, auto&&) {}); REQUIRE_THROWS_AS(op.Completed([](auto&&, auto&&) {}), hresult_illegal_delegate_assignment); } // Action variant carries no result. { - IAsyncAction action = wil::make_ready(); + IAsyncAction action = wil::already_complete(); REQUIRE(action.Status() == AsyncStatus::Completed); action.get(); } // A non-trivial result type round-trips. { - auto op = wil::make_ready(hstring{L"ready"}); + auto op = wil::already_complete(hstring{L"ready"}); REQUIRE(op.get() == L"ready"); } // Failed action: Error status, GetResults/get throw the carried HRESULT. { - IAsyncAction action = wil::make_failed(E_ACCESSDENIED); + IAsyncAction action = wil::already_failed(E_ACCESSDENIED); REQUIRE(action.Status() == AsyncStatus::Error); REQUIRE(action.ErrorCode() == E_ACCESSDENIED); REQUIRE_THROWS_AS(action.get(), hresult_access_denied); @@ -990,7 +990,7 @@ TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") // Failed operation: GetResults throws the carried HRESULT. { - auto op = wil::make_failed(E_INVALIDARG); + auto op = wil::already_failed(E_INVALIDARG); REQUIRE(op.Status() == AsyncStatus::Error); REQUIRE_THROWS_AS(op.GetResults(), hresult_invalid_argument); } From be92201b503cb881d9a86af8ccdd45eb2eb45f4b Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 18:27:11 -0700 Subject: [PATCH 7/8] Null-init ready operation result to avoid activating runtimeclasses The failed-operation path never reads m_result (GetResults throws first), but default-constructing it would activate a projected runtimeclass result -- or fail to compile for a class without a default constructor (e.g. Uri). Init the storage with a null handle for object types and a value-init otherwise. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 20 +++++++++++++++++++- tests/CppWinRTTests.cpp | 21 +++++++++++++++------ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index 1e5d9afc6..a2440fca4 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -593,6 +593,24 @@ namespace details bool m_completed_assigned{false}; }; + // Result storage initializer for the error path, which never reads the value (GetResults throws + // first). For projected WinRT object types (IInspectable/IUnknown-derived) a null handle avoids + // default-activating the runtimeclass; other types (scalars, WinRT structs, hstring) get a cheap + // value-init. Mirrors wil::details::empty, redefined here because that lives under the + // Collections guard while this block only requires Windows.Foundation. + template + T ready_empty_result() noexcept + { + if constexpr (std::is_base_of_v) + { + return nullptr; + } + else + { + return T{}; + } + } + template struct ready_async_operation : ready_async_base< @@ -620,7 +638,7 @@ namespace details } private: - TResult m_result{}; + TResult m_result{ready_empty_result()}; }; struct ready_async_action : diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index 2f93f8058..5cfc4251a 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -988,10 +988,19 @@ TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") REQUIRE_THROWS_AS(action.get(), hresult_access_denied); } - // Failed operation: GetResults throws the carried HRESULT. - { - auto op = wil::already_failed(E_INVALIDARG); - REQUIRE(op.Status() == AsyncStatus::Error); - REQUIRE_THROWS_AS(op.GetResults(), hresult_invalid_argument); - } + // Failed operation: GetResults throws the carried HRESULT. + { + auto op = wil::already_failed(E_INVALIDARG); + REQUIRE(op.Status() == AsyncStatus::Error); + REQUIRE_THROWS_AS(op.GetResults(), hresult_invalid_argument); + } + + // Failed operation whose result is a projected runtimeclass with no default constructor: the + // result storage must be null-initialized (Uri{nullptr}), never activated. A plain value-init + // here would fail to compile, and for default-activatable classes would needlessly activate. + { + auto op = wil::already_failed(E_FAIL); + REQUIRE(op.Status() == AsyncStatus::Error); + REQUIRE_THROWS(op.GetResults()); + } } From 0444b96c861a0aea8a828a2a33817a4d3ad1755d Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 18:34:47 -0700 Subject: [PATCH 8/8] Apply clang-format to changed lines Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- include/wil/cppwinrt_helpers.h | 29 +-- tests/CppWinRTTests.cpp | 314 ++++++++++++++++----------------- 2 files changed, 166 insertions(+), 177 deletions(-) diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index a2440fca4..0140fd960 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -569,8 +569,7 @@ namespace details winrt::Windows::Foundation::AsyncStatus Status() const noexcept { - return (m_error < 0) ? winrt::Windows::Foundation::AsyncStatus::Error - : winrt::Windows::Foundation::AsyncStatus::Completed; + return (m_error < 0) ? winrt::Windows::Foundation::AsyncStatus::Error : winrt::Windows::Foundation::AsyncStatus::Completed; } winrt::hresult ErrorCode() const noexcept @@ -612,16 +611,11 @@ namespace details } template - struct ready_async_operation : - ready_async_base< - ready_async_operation, - winrt::Windows::Foundation::IAsyncOperation, - winrt::Windows::Foundation::AsyncOperationCompletedHandler> + struct ready_async_operation + : ready_async_base, winrt::Windows::Foundation::IAsyncOperation, winrt::Windows::Foundation::AsyncOperationCompletedHandler> { - using base = ready_async_base< - ready_async_operation, - winrt::Windows::Foundation::IAsyncOperation, - winrt::Windows::Foundation::AsyncOperationCompletedHandler>; + using base = + ready_async_base, winrt::Windows::Foundation::IAsyncOperation, winrt::Windows::Foundation::AsyncOperationCompletedHandler>; explicit ready_async_operation(TResult value) : m_result(std::move(value)) { @@ -641,16 +635,11 @@ namespace details TResult m_result{ready_empty_result()}; }; - struct ready_async_action : - ready_async_base< - ready_async_action, - winrt::Windows::Foundation::IAsyncAction, - winrt::Windows::Foundation::AsyncActionCompletedHandler> + struct ready_async_action + : ready_async_base { - using base = ready_async_base< - ready_async_action, - winrt::Windows::Foundation::IAsyncAction, - winrt::Windows::Foundation::AsyncActionCompletedHandler>; + using base = + ready_async_base; ready_async_action() = default; diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index 5cfc4251a..7ce50ecda 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -775,98 +775,98 @@ TEST_CASE("CppWinRTTests::ZStringViewFromHString", "[cppwinrt]") { winrt::hstring hstr = L"Hello"; REQUIRE(wil::zwstring_view(hstr) == hstr); -} -TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") -{ - using namespace winrt::Windows::Foundation::Collections; - - // Indexed collection spanning multiple GetMany blocks (int32 block is 128). - { - std::vector expected; - for (int32_t i = 0; i < 300; ++i) - { - expected.push_back(i); - } - - auto vec = winrt::single_threaded_vector(std::vector(expected)); - - std::vector observed; - for (auto&& value : wil::batched_range(vec)) - { - observed.push_back(value); - } - REQUIRE(observed == expected); - - // The read-only view goes through the same indexed path. - observed.clear(); - for (auto&& value : wil::batched_range(vec.GetView())) - { - observed.push_back(value); - } - REQUIRE(observed == expected); - } - - // Exactly one element beyond a single block boundary. - { - auto vec = winrt::single_threaded_vector(std::vector(129, 7)); - uint32_t count = 0; - for (auto&& value : wil::batched_range(vec)) - { - REQUIRE(value == 7); - ++count; - } - REQUIRE(count == 129); - } - - // Empty collection yields nothing. - { - auto vec = winrt::single_threaded_vector(); - uint32_t count = 0; - for (auto&& value : wil::batched_range(vec)) - { - (void)value; - ++count; - } - REQUIRE(count == 0); - } - - // Iterable-only path (IIterable has no GetAt) buffers through IIterator::GetMany. - { - std::vector expected = {L"a", L"b", L"c"}; - IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); - - std::vector observed; - for (auto&& value : wil::batched_range(iterable)) - { - observed.push_back(value); - } - REQUIRE(observed == expected); - } - - // Directly batching an iterator yields its current position onward. - { - auto vec = winrt::single_threaded_vector({1, 2, 3, 4, 5}); - std::vector observed; - for (auto&& value : wil::batched_range(vec.First())) - { - observed.push_back(value); - } - REQUIRE(observed == std::vector({1, 2, 3, 4, 5})); - } - - // Map batches over IKeyValuePair through the iterable path. - { - std::map src{{L"kittens", L"fluffy"}, {L"puppies", L"cute"}}; - auto map = winrt::single_threaded_map(std::map(src)); - uint32_t count = 0; - for (auto&& pair : wil::batched_range(map)) - { - REQUIRE(pair.Value() == src.at(pair.Key())); - ++count; - } - REQUIRE(count == src.size()); - } - +} +TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") +{ + using namespace winrt::Windows::Foundation::Collections; + + // Indexed collection spanning multiple GetMany blocks (int32 block is 128). + { + std::vector expected; + for (int32_t i = 0; i < 300; ++i) + { + expected.push_back(i); + } + + auto vec = winrt::single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : wil::batched_range(vec)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + + // The read-only view goes through the same indexed path. + observed.clear(); + for (auto&& value : wil::batched_range(vec.GetView())) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Exactly one element beyond a single block boundary. + { + auto vec = winrt::single_threaded_vector(std::vector(129, 7)); + uint32_t count = 0; + for (auto&& value : wil::batched_range(vec)) + { + REQUIRE(value == 7); + ++count; + } + REQUIRE(count == 129); + } + + // Empty collection yields nothing. + { + auto vec = winrt::single_threaded_vector(); + uint32_t count = 0; + for (auto&& value : wil::batched_range(vec)) + { + (void)value; + ++count; + } + REQUIRE(count == 0); + } + + // Iterable-only path (IIterable has no GetAt) buffers through IIterator::GetMany. + { + std::vector expected = {L"a", L"b", L"c"}; + IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : wil::batched_range(iterable)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Directly batching an iterator yields its current position onward. + { + auto vec = winrt::single_threaded_vector({1, 2, 3, 4, 5}); + std::vector observed; + for (auto&& value : wil::batched_range(vec.First())) + { + observed.push_back(value); + } + REQUIRE(observed == std::vector({1, 2, 3, 4, 5})); + } + + // Map batches over IKeyValuePair through the iterable path. + { + std::map src{{L"kittens", L"fluffy"}, {L"puppies", L"cute"}}; + auto map = winrt::single_threaded_map(std::map(src)); + uint32_t count = 0; + for (auto&& pair : wil::batched_range(map)) + { + REQUIRE(pair.Value() == src.at(pair.Key())); + ++count; + } + REQUIRE(count == src.size()); + } + // Non-WinRT indexed shape works too, matching to_vector's duck typing. { uint32_t count = 0; @@ -923,71 +923,71 @@ TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") } REQUIRE(observed == std::vector({20, 30, 40})); } -} - -TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") -{ - using namespace winrt; - using namespace winrt::Windows::Foundation; - - // Completed synchronously with a value, with no coroutine frame. - { - IAsyncOperation op = wil::already_complete(42); - REQUIRE(op.Status() == AsyncStatus::Completed); - REQUIRE(op.ErrorCode() == 0); - REQUIRE(op.GetResults() == 42); - REQUIRE(op.get() == 42); - } - - // co_await yields the value through the synchronous-completion path. - { - auto coro = []() -> IAsyncOperation { - co_return co_await wil::already_complete(7); - }; - REQUIRE(coro().get() == 7); - } - - // A Completed handler on an already-completed operation fires immediately. - { - auto op = wil::already_complete(5); - int32_t observed = 0; - AsyncStatus observed_status = AsyncStatus::Started; - op.Completed([&](IAsyncOperation const& sender, AsyncStatus status) { - observed = sender.GetResults(); - observed_status = status; - }); - REQUIRE(observed == 5); - REQUIRE(observed_status == AsyncStatus::Completed); - } - - // Assigning Completed twice is illegal, matching the coroutine promise. - { - auto op = wil::already_complete(1); - op.Completed([](auto&&, auto&&) {}); - REQUIRE_THROWS_AS(op.Completed([](auto&&, auto&&) {}), hresult_illegal_delegate_assignment); - } - - // Action variant carries no result. - { - IAsyncAction action = wil::already_complete(); - REQUIRE(action.Status() == AsyncStatus::Completed); - action.get(); - } - - // A non-trivial result type round-trips. - { - auto op = wil::already_complete(hstring{L"ready"}); - REQUIRE(op.get() == L"ready"); - } - - // Failed action: Error status, GetResults/get throw the carried HRESULT. - { - IAsyncAction action = wil::already_failed(E_ACCESSDENIED); - REQUIRE(action.Status() == AsyncStatus::Error); - REQUIRE(action.ErrorCode() == E_ACCESSDENIED); - REQUIRE_THROWS_AS(action.get(), hresult_access_denied); - } - +} + +TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") +{ + using namespace winrt; + using namespace winrt::Windows::Foundation; + + // Completed synchronously with a value, with no coroutine frame. + { + IAsyncOperation op = wil::already_complete(42); + REQUIRE(op.Status() == AsyncStatus::Completed); + REQUIRE(op.ErrorCode() == 0); + REQUIRE(op.GetResults() == 42); + REQUIRE(op.get() == 42); + } + + // co_await yields the value through the synchronous-completion path. + { + auto coro = []() -> IAsyncOperation { + co_return co_await wil::already_complete(7); + }; + REQUIRE(coro().get() == 7); + } + + // A Completed handler on an already-completed operation fires immediately. + { + auto op = wil::already_complete(5); + int32_t observed = 0; + AsyncStatus observed_status = AsyncStatus::Started; + op.Completed([&](IAsyncOperation const& sender, AsyncStatus status) { + observed = sender.GetResults(); + observed_status = status; + }); + REQUIRE(observed == 5); + REQUIRE(observed_status == AsyncStatus::Completed); + } + + // Assigning Completed twice is illegal, matching the coroutine promise. + { + auto op = wil::already_complete(1); + op.Completed([](auto&&, auto&&) {}); + REQUIRE_THROWS_AS(op.Completed([](auto&&, auto&&) {}), hresult_illegal_delegate_assignment); + } + + // Action variant carries no result. + { + IAsyncAction action = wil::already_complete(); + REQUIRE(action.Status() == AsyncStatus::Completed); + action.get(); + } + + // A non-trivial result type round-trips. + { + auto op = wil::already_complete(hstring{L"ready"}); + REQUIRE(op.get() == L"ready"); + } + + // Failed action: Error status, GetResults/get throw the carried HRESULT. + { + IAsyncAction action = wil::already_failed(E_ACCESSDENIED); + REQUIRE(action.Status() == AsyncStatus::Error); + REQUIRE(action.ErrorCode() == E_ACCESSDENIED); + REQUIRE_THROWS_AS(action.get(), hresult_access_denied); + } + // Failed operation: GetResults throws the carried HRESULT. { auto op = wil::already_failed(E_INVALIDARG);