<flat_set>: Test transparent insertion and more#4084
<flat_set>: Test transparent insertion and more#4084StephanTLavavej merged 12 commits intomicrosoft:feature/flat_setfrom
<flat_set>: Test transparent insertion and more#4084Conversation
…` standard-conformant
stl/inc/flat_set
Outdated
| // precondition: `find(_Val) == find(_Keyval)` (per N4958 [flat.set.modifiers]/2) | ||
| // the precondition cannot guarantee `_Can_insert(_Where, _Keyval)` here. | ||
| _STL_ASSERT(!contains(_Keyval), "find(_Val) == end(), but find(_Keyval) != end()"); | ||
| return pair{_Emplace_hint(_Where, _STD move(_Keyval)), true}; |
There was a problem hiding this comment.
[.] Preconditions: The conversion from x into value_type constructs an object u, for which find(x) == find(u) is true.
I think this precondition is poorly-specified... or, does the standard intentionally decide to allow those "partially inconsistent" cases?
There was a problem hiding this comment.
What the Standard should say is that the transparent comparison returns results that are exactly as good as the object that will be constructed eventually. That is, I think it's undesirable to add weird code to handle "partially inconsistent" comparison results. We should instead file an LWG issue instead of trying to handle phantom cases that shouldn't occur with proper usage, especially if doing so would reduce efficiency. The whole point of transparent lookup is efficiency, to avoid having to construct real keys ahead of time.
There was a problem hiding this comment.
I made a mistake when reasoning about inconsistency - there is no "partially" inconsistent types. I thought (2) below is a special trait for certain types, so I coined "partially"; now I realize (2) is generally true for all types under current precondition. The situation is that:
- When a
transparent-keytype represents different things whencomparing with key_type(per the comparator) and whenconvertig to key_type(per the type itself), it makes logical inconsistency. - However, there are
alwayssome situations where the input will not trigger precondition violation, depending on both the content in flat_set and the input. This makes the implementation unable to conceptually reject any types.
An obvious way to show how (2) is true and how inconsistent types are broken:
// empty set:
flat_set<key_type, transparent-comparator> fs;
// will never trigger precondition violation. The set is empty, so find(anything) == end().
fs.insert(arbitary-broken transparent-key);
// the same expression will violate the precondition now, as transparent-find still == end(), but find(conversion-result) != end() now.
fs.insert(the same input as above);
Preconditions: The conversion from x into value_type constructs an object u, for which find(x) == find(u) is true.
As to the precondition for transparent key, I think instead of requiring certain expressions to be true, it should be undefined behavior as long as the transparent key logically represents different things when comparing with key_type (per the comparator) and when convertig to key_type (per the type itself).
| explicit operator int() const { | ||
| return key - 10; | ||
| } | ||
| }; |
There was a problem hiding this comment.
I really hope to see cases like this being totally prohibited by the standard one day :|
This comment was marked as resolved.
This comment was marked as resolved.
stl/inc/flat_set
Outdated
| // precondition: `find(_Val) == find(_Keyval)` (per N4958 [flat.set.modifiers]/2) | ||
| // the precondition cannot guarantee `_Can_insert(_Where, _Keyval)` here. | ||
| _STL_ASSERT(!contains(_Keyval), "find(_Val) == end(), but find(_Keyval) != end()"); | ||
| return pair{_Emplace_hint(_Where, _STD move(_Keyval)), true}; |
There was a problem hiding this comment.
What the Standard should say is that the transparent comparison returns results that are exactly as good as the object that will be constructed eventually. That is, I think it's undesirable to add weird code to handle "partially inconsistent" comparison results. We should instead file an LWG issue instead of trying to handle phantom cases that shouldn't occur with proper usage, especially if doing so would reduce efficiency. The whole point of transparent lookup is efficiency, to avoid having to construct real keys ahead of time.
Co-authored-by: Jakub Mazurkiewicz <mazkuba3@gmail.com>
| if (_Where != _End && !_Compare(_Val, *_Where)) { | ||
| // convert const_iterator to iterator | ||
| // *_Where is equivalent to _Val; convert _Where to iterator type. | ||
| return _Mycont.begin() + (_Where - _Begin); |
There was a problem hiding this comment.
(not planned in this pr) btw, this conversion can be avoided after using container::const_iterator for iterator type.
| explicit _Base_flat_set(const _Alloc& _Al) : _Mycont(_Al), _Mycomp() {} | ||
|
|
||
| // TRANSITION, an allocator-aware container may not support "C(_First, _Last, _Al)". | ||
| // FIXME, an allocator-aware container may not support "C(_First, _Last, _Al)". |
There was a problem hiding this comment.
From a previous comment (#4064 (comment)):
I think there is no need to use
make_obj_using_allocatorforC(another,al) and C(al), as these constructors are required for allocator-aware containers. I believe a container thatuses_allocator_vthe allocator should also support these constructors.
These constructors don't seem to be only required to work with allocator-aware constructors (which is weird).
There may be some non-allocator-aware containers supporting C(allocator_arg, al, another), and we should support them.
But the
flat_setconstructors only say the container should be constructed with the allocator, then behave the same as without the allocator (inserting the range). So these constructors should be valid even if the container doesn't directly support these constructions.Effects: Equivalent to the corresponding non-allocator constructors except that c is constructed with uses-allocator construction ([allocator.uses.construction]).
I don't see why these constructors should be valid when uses-allocator construction is ill-formed. Note that as of C++20, uses-allocator construction is defined in term of make_obj_using_allocator ([allocator.uses.construction]/1).
I suggest consistently using make_obj_using_allocator for flat_meow (which is, however, inconsistent with old container adaptors). We may do this in the next PR.
There was a problem hiding this comment.
Effects: Equivalent to the corresponding non-allocator constructors except that c is constructed with uses-allocator construction
template
flat_set(InputIterator first, InputIterator last, const key_compare& comp = key_compare())
: c(), compare(comp)
{ insert(first, last); }
template<container-compatible-range<value_type> R>
flat_set(from_range_t, R&& rg, const key_compare& comp)
: flat_set(comp)
{ insert_range(std::forward(rg)); }
Take these ctors for example... Actually most without-allocator ctors don't rely on container's ctors (though they should work fine as they are required by sequential containers, so are safely appliable as optimizations for without-allocator ctors), so I'm thinking the with-allocator versions should always work fine too, as only c(allocator) is required to be valid.
I'm not very sure whether uses_allocator_v an allocator-type is able to ensure the validity of c(allocator), though. If a container is allocator-aware, then c(allocator) and c(c2,allocator) won't need make_obj_using_allocator, but it seems there is no relation between uses_allocator_v and allocator-awareness?
There was a problem hiding this comment.
as only
c(allocator)is required to be valid
Perhaps this is not even required. Moreover, I don't think flat container adaptors actually require the adapted containers to be allocator-aware anywhere.
I'm not very sure whether
uses_allocator_van allocator-type is able to ensure the validity ofc(allocator), though.
The answer is no to me... But given constructors of containers and container adaptors are generally underconstrained, we shouldn't be worried.
There was a problem hiding this comment.
c is constructed with uses-allocator construction
Do you think the standard should give more details about this, especially, what should be passed as arguments?
There was a problem hiding this comment.
c is constructed with uses-allocator construction
Do you think the standard should give more details about this, especially, what should be passed as arguments?
Yes, perhaps there should be an editorial or LWG issue.
I think the current specification should be interpreted as mapping c(args...) to c(std::make_obj_using_allocator<container_type>(al, args...)). In the case of c(args...), the passed arguments are fully specified. But the mapping itself is a bit unclear.
<flat_set>: make transparent insertion standard-conformant<flat_set>: Test transparent insertion and more
🥞 ✅ 🔼 |
(extracted from pr 4079)
Try to resolve an issue:be very conservative aboutflat_set's transparent insertion, so that the methods can deal with partially inconsistent cases.(That is,find(val) == find(key(val)), as specified by the standard, butboth == end(), and lower_bound(val) != lower_bound(key(val)). I believe this is an underspecification that will make the function a lot less likely efficient...)The problem is more of a defect in the standard; filed
flat_set's transparent insertion requirements are deficient #4105 instead.