Skip to content

<flat_set>: Test transparent insertion and more#4084

Merged
StephanTLavavej merged 12 commits intomicrosoft:feature/flat_setfrom
achabense:_Flat_set_5_b
Oct 23, 2023
Merged

<flat_set>: Test transparent insertion and more#4084
StephanTLavavej merged 12 commits intomicrosoft:feature/flat_setfrom
achabense:_Flat_set_5_b

Conversation

@achabense
Copy link
Contributor

@achabense achabense commented Oct 11, 2023

(extracted from pr 4079)

  • Add more test coverages.
  • Try to resolve an issue:
    be very conservative about flat_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, but both == 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.

@achabense achabense requested a review from a team as a code owner October 11, 2023 16:06
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};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[.] 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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made tracking issue #4105.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. When a transparent-key type represents different things when comparing with key_type (per the comparator) and when convertig to key_type (per the type itself), it makes logical inconsistency.
  2. However, there are always some 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;
}
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really hope to see cases like this being totally prohibited by the standard one day :|

@StephanTLavavej StephanTLavavej added the flat_meow C++23 container adaptors label Oct 11, 2023
@StephanTLavavej StephanTLavavej self-assigned this Oct 11, 2023
@StephanTLavavej

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};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@StephanTLavavej StephanTLavavej removed their assignment Oct 19, 2023
achabense and others added 2 commits October 19, 2023 09:08
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);
Copy link
Contributor Author

@achabense achabense Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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)".
Copy link
Contributor

@frederick-vs-ja frederick-vs-ja Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a previous comment (#4064 (comment)):

I think there is no need to use make_obj_using_allocator for C(another,al) and C(al), as these constructors are required for allocator-aware containers. I believe a container that uses_allocator_v the 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_set constructors 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.

Copy link
Contributor Author

@achabense achabense Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_v an allocator-type is able to ensure the validity of c(allocator), though.

The answer is no to me... But given constructors of containers and container adaptors are generally underconstrained, we shouldn't be worried.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@StephanTLavavej StephanTLavavej self-assigned this Oct 19, 2023
@StephanTLavavej StephanTLavavej changed the title <flat_set>: make transparent insertion standard-conformant <flat_set>: Test transparent insertion and more Oct 23, 2023
@StephanTLavavej StephanTLavavej merged commit a75edc9 into microsoft:feature/flat_set Oct 23, 2023
@StephanTLavavej
Copy link
Member

🥞 ✅ 🔼

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

Labels

flat_meow C++23 container adaptors

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants