Skip to content

Commit 87436b4

Browse files
Fix ABI conformance of IMap::Remove, add TryRemove (#655)
* Fix ABI conformance of IMap::Remove, add TryRemove `single_threaded_map()`'s IMap::Remove did not throw `hresult_out_of_range` on attempts to remove a nonexistent key. Now it throws. Failure to remove a nonexistent key does not invalidate iterators, because nothing actually changed. This brings the map implementation in line with the implementations in other projections. Note that this is a breaking change. Code that assumed nonexistent objects could be harmlessly removed will encounter exceptions when run against C++/WinRT implementations. This was, however, a pre-existing bug, because implementations from other projections (C#, C++/CX) always threw under those conditions. Added a TryRemove() method for people who wanted the nonthrowing version. Note that fixing the ABI conformance is required in order for TryRemove to work, because TryRemove relies on the call to Remove failing if the key doesn't exist. (JavaScript doesn't project objects as maps, so there is nothing to validate there.) Tightened the behavior of TryLookup and TryRemove so they propagate RPC failures. Because the inability to remove the item could be due to the server being unavailable, and that's not the same as the item not existing in the collection. Previous code treated loss of server the same as "The item doesn't exist", which is not true: The item could exist, we just were unable to contact the server to find out. * TryLookup and TryRemove should not be noexcept because they can throw on other errors. Added unit test to verify that errors other than "key not found" are propagated. Co-authored-by: Kenny Kerr <[email protected]>
1 parent f816245 commit 87436b4

File tree

6 files changed

+75
-12
lines changed

6 files changed

+75
-12
lines changed

cppwinrt/code_writers.h

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,20 +1305,20 @@ namespace cppwinrt
13051305
else if (type_name == "Windows.Foundation.Collections.IMapView`2")
13061306
{
13071307
w.write(R"(
1308-
auto TryLookup(param_type<K> const& key) const noexcept
1308+
auto TryLookup(param_type<K> const& key) const
13091309
{
13101310
if constexpr (std::is_base_of_v<Windows::Foundation::IUnknown, V>)
13111311
{
13121312
V result{ nullptr };
1313-
WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMapView<K, V>)->Lookup(get_abi(key), put_abi(result));
1313+
impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMapView<K, V>)->Lookup(get_abi(key), put_abi(result)));
13141314
return result;
13151315
}
13161316
else
13171317
{
13181318
std::optional<V> result;
13191319
V value{ empty_value<V>() };
13201320
1321-
if (0 == WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMapView<K, V>)->Lookup(get_abi(key), put_abi(value)))
1321+
if (0 == impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMapView<K, V>)->Lookup(get_abi(key), put_abi(value))))
13221322
{
13231323
result = std::move(value);
13241324
}
@@ -1331,27 +1331,32 @@ namespace cppwinrt
13311331
else if (type_name == "Windows.Foundation.Collections.IMap`2")
13321332
{
13331333
w.write(R"(
1334-
auto TryLookup(param_type<K> const& key) const noexcept
1334+
auto TryLookup(param_type<K> const& key) const
13351335
{
13361336
if constexpr (std::is_base_of_v<Windows::Foundation::IUnknown, V>)
13371337
{
13381338
V result{ nullptr };
1339-
WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap<K, V>)->Lookup(get_abi(key), put_abi(result));
1339+
impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap<K, V>)->Lookup(get_abi(key), put_abi(result)));
13401340
return result;
13411341
}
13421342
else
13431343
{
13441344
std::optional<V> result;
13451345
V value{ empty_value<V>() };
13461346
1347-
if (0 == WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap<K, V>)->Lookup(get_abi(key), put_abi(value)))
1347+
if (0 == impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap<K, V>)->Lookup(get_abi(key), put_abi(value))))
13481348
{
13491349
result = std::move(value);
13501350
}
13511351
13521352
return result;
13531353
}
13541354
}
1355+
1356+
auto TryRemove(param_type<K> const& key) const
1357+
{
1358+
return 0 == impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap<K, V>)->Remove(get_abi(key)));
1359+
}
13551360
)");
13561361
}
13571362
else if (type_name == "Windows.Foundation.IAsyncAction")

strings/base_collections_base.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
WINRT_EXPORT namespace winrt
32
{
43
template <typename D, typename T, typename Version = impl::no_collection_version>
@@ -415,8 +414,14 @@ WINRT_EXPORT namespace winrt
415414

416415
void Remove(K const& key)
417416
{
417+
auto& container = static_cast<D&>(*this).get_container();
418+
auto found = container.find(static_cast<D const&>(*this).wrap_value(key));
419+
if (found == container.end())
420+
{
421+
throw hresult_out_of_bounds();
422+
}
418423
this->increment_version();
419-
static_cast<D&>(*this).get_container().erase(static_cast<D const&>(*this).wrap_value(key));
424+
container.erase(found);
420425
}
421426

422427
void Clear() noexcept

strings/base_error.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,3 +579,15 @@ WINRT_EXPORT namespace winrt
579579
abort();
580580
}
581581
}
582+
583+
namespace winrt::impl
584+
{
585+
inline hresult check_hresult_allow_bounds(hresult const result)
586+
{
587+
if (result != impl::error_out_of_bounds)
588+
{
589+
check_hresult(result);
590+
}
591+
return result;
592+
}
593+
}

test/old_tests/UnitTests/TryLookup.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,41 @@ TEST_CASE("TryLookup")
9090
REQUIRE(map.TryLookup(123).value() == 456);
9191
}
9292
}
93+
94+
TEST_CASE("TryRemove")
95+
{
96+
auto map = single_threaded_map<int, IStringable>(std::map<int, IStringable>{
97+
{ 123, nullptr },
98+
{ 124, make<stringable>(L"remove") },
99+
{ 125, make<stringable>(L"keep") },
100+
});
101+
102+
REQUIRE(map.TryRemove(122) == false);
103+
REQUIRE(map.TryRemove(123) == true);
104+
REQUIRE(map.TryRemove(124) == true);
105+
106+
// Should still have one item left.
107+
REQUIRE(map.Size() == 1);
108+
REQUIRE(map.Lookup(125).ToString() == L"keep");
109+
}
110+
111+
TEST_CASE("TryLookup TryRemove error")
112+
{
113+
// Simulate a non-agile map that is being accessed from the wrong thread.
114+
// "Try" operations should throw rather than erroneously report "not found".
115+
// Because they didn't even try. The operation never got off the ground.
116+
struct incorrectly_used_non_agile_map : implements<incorrectly_used_non_agile_map, IMap<int, int>>
117+
{
118+
int Lookup(int) { throw hresult_wrong_thread(); }
119+
int32_t Size() { throw hresult_wrong_thread(); }
120+
bool HasKey(int) { throw hresult_wrong_thread(); }
121+
IMapView<int, int> GetView() { throw hresult_wrong_thread(); }
122+
bool Insert(int, int) { throw hresult_wrong_thread(); }
123+
void Remove(int) { throw hresult_wrong_thread(); }
124+
void Clear() { throw hresult_wrong_thread(); }
125+
};
126+
127+
auto map = make<incorrectly_used_non_agile_map>();
128+
REQUIRE_THROWS_AS(map.TryLookup(123), hresult_wrong_thread);
129+
REQUIRE_THROWS_AS(map.TryRemove(123), hresult_wrong_thread);
130+
}

test/old_tests/UnitTests/produce_map.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ TEST_CASE("produce_IMap_int32_t_hstring")
9393
REQUIRE(m.Size() == 2);
9494
m.Remove(1); // existing
9595
REQUIRE(m.Size() == 1);
96-
m.Remove(3); // not existing
96+
REQUIRE_THROWS_AS(m.Remove(3), hresult_out_of_bounds); // not existing
9797
REQUIRE(m.Size() == 1);
9898

9999
m.Clear();
@@ -177,7 +177,8 @@ TEST_CASE("produce_IMap_hstring_int32_t")
177177
REQUIRE(m.Size() == 2);
178178
m.Remove(L"one"); // existing
179179
REQUIRE(m.Size() == 1);
180-
m.Remove(L"three"); // not existing
180+
REQUIRE_THROWS_AS(m.Remove(L"three"), hresult_out_of_bounds); // not existing
181+
REQUIRE(!m.TryRemove(L"three")); // not existing
181182
REQUIRE(m.Size() == 1);
182183

183184
m.Clear();

test/old_tests/UnitTests/single_threaded_map.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace
2828
values.Insert(2,20);
2929
values.Insert(3,30);
3030
IIterator<IKeyValuePair<int, int>> first = values.First();
31+
REQUIRE(!values.TryRemove(999)); // failed removal does not invalidate
3132
REQUIRE(first.HasCurrent());
3233
[[maybe_unused]] auto pair = first.Current();
3334
REQUIRE(first.MoveNext());
@@ -52,7 +53,8 @@ namespace
5253
REQUIRE(!values.Insert(2, 20));
5354
compare(values, { { 1,100 }, {2,20} });
5455

55-
values.Remove(3);
56+
REQUIRE_THROWS_AS(values.Remove(3), hresult_out_of_bounds);
57+
REQUIRE(!values.TryRemove(3));
5658
compare(values, { { 1,100 },{ 2,20 } });
5759
values.Remove(2);
5860
compare(values, { { 1,100 } });
@@ -65,7 +67,7 @@ namespace
6567
compare(values, {});
6668

6769
test_invalidation(values, [&] { values.Clear(); });
68-
test_invalidation(values, [&] { values.Remove(10); });
70+
test_invalidation(values, [&] { values.Remove(1); });
6971
test_invalidation(values, [&] { values.Insert(1,10); });
7072
}
7173
}

0 commit comments

Comments
 (0)