-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comparison concepts and reference types #155
Comments
I'll notice that in some sense the issue is worse now in the current draft, which is now internally inconsistent. In |
We didn't understand when first formulating the concepts that the parameter types of requires expressions undergo decay: template<class T>
concept bool C = requires(T t) {
t = t;
};
static_assert(C<void(int)>); // Oops; function types are not assignable, but function *pointers* are
static_assert(C<int[42]>); // Oops: array vs pointer-to-element I've been gradually armoring the concepts against array- and function-to-pointer decay, which I see now was a mistake. I should have made a single global fix. |
@CaseyCarter I'd like to bump this one up the queue, especially since we're monkeying with the object concepts again. |
|
P0547 brings the 'structible and object concepts into line, absolutely. The Core concepts were already in good shape - That leaves only the Comparison concepts in question. Should |
Let's look at a specific example: template <> struct equal_to<void> {
template <class T, class U>
requires EqualityComparable<T, U>()
constexpr auto operator()(T&& t, U&& u) const
-> decltype(std::forward<T>(t) == std::forward<U>(u));
typedef unspecified is_transparent;
}; If I'm extremely uncomfortable with concept checks that introduce hard errors. A type should satisfy the concept or not. There is no third option. I think I would be OK with saying that reference types do not satisfy these concepts. Although it's less convenient, it does avoid potential confusion. |
P.S. Why does |
We wanted
Hard errors exist to prevent us from doing something stupid when there is no "right" answer: struct incomplete;
Destructible<incomplete>(); // Should be ill-formed
Assignable<incomplete&, int>(); // Ditto
struct complete { complete() = default; complete(complete const&) = delete; };
Swappable<complete&, complete&>(); // Ok (false)
void swap(complete&, complete&) {}
Swappable<complete&, complete&>(); // Should be Ill-formed NDR |
...which I suppose applies equally well to |
template <> struct equal_to<void> {
template <class T, class U>
requires EqualityComparable<T, U>()
constexpr auto operator()(T&& t, U&& u) const
-> decltype(std::forward<T>(t) == std::forward<U>(u));
typedef unspecified is_transparent;
}; This is the essential "dangerous scenario" that concerns me: people applying concepts to forwarding references and thinking that means the referent type satisfies the concept. (I've called this "reference confusion" before.) This is what makes it tempting to declare e.g. In the particular case of the comparison concepts, if we make them either require object types or properly strip references, it makes this error impossible: |
Right now, I'm convinced that |
I could imagine a future time when we want to separate "boolean-testability" from the stronger "behaves like a bool". Then |
I'm making these edits to cmcstl2, and it turned up an issue. |
Perhaps instead of allowing reference types to satisfy |
I don't like the implications for the algorithms. Would they have to explicitly |
We could strip referenceness from the requires expression parameters, and require |
|
No. The comparison concepts require e.g.
The type of |
I don't think we care to distinguish xvalues from prvalues; it should not matter for our uses. EDIT: This is also much better than the replacement EDIT AGAIN: |
…icniebler/stl2#155); remove argument deduction constraint work-arounds; fix up deduction constraints (refs ericniebler/stl2#331)
…icniebler/stl2#155); remove argument deduction constraint work-arounds; fix up deduction constraints (refs ericniebler/stl2#331)
…icniebler/stl2#155); remove argument deduction constraint work-arounds; fix up deduction constraints (refs ericniebler/stl2#331)
…icniebler/stl2#155); remove argument deduction constraint work-arounds; fix up deduction constraints (refs ericniebler/stl2#331)
…ance (#76) * Fix up Boolean, EqualityComparable, and StrictTotallyOrdered (refs ericniebler/stl2#155); remove argument deduction constraint work-arounds; fix up deduction constraints (refs ericniebler/stl2#331) * fix Assignable per discussion with @CaseyCarter
The PR removes |
Now |
* P0541 * P0547 * P0579 * ericniebler/stl2#155 * ericniebler/stl2#167 * ericniebler/stl2#172 * ericniebler/stl2#229 * ericniebler/stl2#232 * ericniebler/stl2#239 * ericniebler/stl2#241 * ericniebler/stl2#242 * ericniebler/stl2#243 * ericniebler/stl2#244 * ericniebler/stl2#245 * ericniebler/stl2#255 * ericniebler/stl2#286 * ericniebler/stl2#299 * ericniebler/stl2#301 * ericniebler/stl2#310 * ericniebler/stl2#311 * ericniebler/stl2#313 * ericniebler/stl2#322 * ericniebler/stl2#339 * ericniebler/stl2#381 Remove post-increment experiment in `move_iterator`. Remove `EqualityComparable`/`Sentinel<default_sentinel>` extensions to `ostreambuf_iterator`.
* P0541 * P0547 * P0579 * ericniebler/stl2#155 * ericniebler/stl2#167 * ericniebler/stl2#172 * ericniebler/stl2#229 * ericniebler/stl2#232 * ericniebler/stl2#239 * ericniebler/stl2#241 * ericniebler/stl2#242 * ericniebler/stl2#243 * ericniebler/stl2#244 * ericniebler/stl2#245 * ericniebler/stl2#255 * ericniebler/stl2#286 * ericniebler/stl2#299 * ericniebler/stl2#301 * ericniebler/stl2#310 * ericniebler/stl2#311 * ericniebler/stl2#313 * ericniebler/stl2#322 * ericniebler/stl2#339 * ericniebler/stl2#381 Remove post-increment experiment in `move_iterator`. Remove `EqualityComparable`/`Sentinel<default_sentinel>` extensions to `ostreambuf_iterator`.
We have quite a few concepts in the TS that are well-defined over value types - un-cv-qualified non-array object types - whose meaning is unclear for non-value types. We need to determine what their meaning should be. I'll examine
EqualityComparable
as a representative example in hopes that whatever conclusion I reach can be generalized to cover this entire class of concepts.EqualityComparable
EqualityComparable<foo>()
has a straight-forward meaning for a value typefoo
:bool(a == b)
iffa
equalsb
(==
means "equals")bool(a != b) == !bool(a == b)
(As relations,!=
is the complement of==
)a == b
anda != b
are valid non-modifying equality-preserving expressions if botha
andb
are expressions with typefoo
orconst foo
and any value category.(Aside: The requirement "
bool(a == a)
" is not axiomatic; it's implied by "bool(a == b)
iffa
equalsb
". It also seems reasonable to me to require that the definition spaces of the two expressions be equal. Neither of these observations is in scope for this particular discussion.)What meaning, if any, should
EqualityComparable<const foo>()
have?EqualityComparable<foo&>()
?EqualityComparable<volatile foo&&>()
?Status Quo
What meaning does the current specification of
EqualityComparable
ascribe to non-value types?a
andb
be objects of typeT
," doesn't have a well-defined meaning whenT
is a non-object type.T
is a function or array type, the parameters of the requires clause are effectively of typeconst decay_t<T>
; parameters are decayed just as function parameters are. As a result,EqualityComparable<int(double)>() == EqualityComparable<int(*)(double)>()
andEqualityComparable<int[42]>() == EqualityComparable<int*>()
, neither of which seems sensible to me since we cannot directly compare functions nor arrays for equality in C++. Even more bizarre is the cross-type case, whereEqualityComparable<int[13],int[42]>() == EqualityComparable<int*,int*>()
.T
is a reference type,const T
is the same type asT
, so the required expressions must be valid for lvalues of typeremove_reference_t<T>
. Ifremove_reference_t<T>
is notconst
, the expressions may modify the operands and they do not implicitly require expression variants.T
actually seems meaningful. "a
equalsb
" is defined for values, and object types that differ in their cv-qualifiers have the same value type.const const T
is the same type asconst T
so the required expressions are exactly equivalent to the expressions required by unqualifiedT
whenT
isconst
-qualified. This would not be the case for a concept with expressions that are intended to allow modification. Given a hypothetical conceptC<T>
that requires an expressionf(t)
that may modifyt
,C<const T>
requiresf(ct)
to be non-modifying and have implicit expression variants that also must be non-modifying. It's subtle that a template with associated constraints that include bothC<foo>
andC<const foo>
actually forbidsf
to modify a mutable lvaluefoo
.The case for
volatile
is similar: the explicitly required expressions operate on lvalues of typeconst volatile T
, so implicit expression variants are required for all value categories of expressions with typeconst volatile T
orvolatile T
, but notably not typesT
orconst T
. This could be addressed by extending the wording for implicit expression variants to generally cover all less-cv-qualified types.The same considerations apply for
C<volatile foo>
andC<const volatile foo>
as noted above forC<foo>
andC<const foo>
.Note that the expression stability requirement in [concepts.lib.general.equality]/3 effectively disables the meaning of
volatile
in equality-preserving expressions: an external process or action may change the value of avolatile
object, but it's forbidden to do so when the library may observe it. This distinction is subtle and may cause confusion if value-semantic concepts admitvolatile
types.Functions & Arrays
The consequences of allowing function and array types are both bizarre and subtle, I believe the entire class of value-semantic concepts should categorically reject them.
References
The effects of allowing reference types are also quite subtle. The vast majority of non-language lawyers don't know how cv-qualifiers apply to reference types and won't have a chance of intuiting what
EqualityComparable<Foo&&>
means. Admitting reference types leads to a particular logic error I call "type/reference confusion": it's not clear to the reader or writer of code whether the intent is that the concept requirements apply to the type of the reference, or apply to the type of the referent. Proxy reference types exacerbate this problem in that they aren't easily distinguishable as references. If users become accustomed toC<Foo&>
having the same meaning asC<Foo>
, they will be caught unawares when e.g.C<reference_wrapper<Foo>>
does not.Until we have more widespread user experience with the TS I think it's best that the value-semantic class of concepts not admit reference types. What's not clear to me is whether the concepts should reject reference types syntactically, or if a program that applies such a concept to a reference type should be ill-formed. I've used both techniques for various concepts in the implementation of the TS at different times. My experience - which can hardly be considered statistically representative - is that hard errors usually occur closer to the source of the problem, and that silent rejections result in hard to find bugs buried beneath layers of templates and concepts.
cv-qualifiers
I'm least certain about how to approach cv-qualifiers. I can see a path that will give them meaning - much as we did with the object concepts - although there are still some subtleties. How we choose to handle cv-qualifiers has ramifications for the rest of the design. An
Iterator
cannot currently have avolatile
-qualified reference type, for example, and allowing that would require changes throughout the algorithm specifications. Supportingvolatile
-qualified types in the concepts provides little value if the rest of the library doesn't support them, and changing the entire TS to support a freakish corner case is out of the question.const
-qualified types are easily supportable with reasonable semantics, and less marginal thanvolatile
types. I'm fairly certain the existing wording of the library concepts works withconst
types and would require no changes. Forbidding reference types and volatile qualifiers while allowingconst
does seem like a special case to me, and therefore undesirable for consistency's sake.Conclusions
If I had to choose an approach right now I would keep the meaning of the concepts as sharply defined as possible by rejecting non-object types, array types, and cv-qualified types syntactically, while making reference types ill-formed. Users must then learn that satisfaction of
C<remove_const_t<T>>()
implies that all of the required non-modifying expressions are valid forT
, but the required maybe-modifying expressions are only valid ifT
is notconst
. Forcing users to deal with cv-qualifiers and reference types explicitly makes it harder to write generic code, but simultaneously makes it easier to write generic code with fewer errors.Proposed Resolution
Change the definition of the
Booolean
concept ([concepts.lib.compare.boolean]/p1) as follows (includes the resolution for #330):Change [concepts.lib.compare.boolean]/p2 as follows (depends on the resolution of #167):
Change concept
WeaklyEqualityComparable
([concepts.lib.compare.equalitycomparable]) as follows (includes the resolution for #330):Change [concepts.lib.compare.equalitycomparable]/p1 as follows:
Change cross-type concept
EqualityComparable
([concepts.lib.compare.equalitycomparable]) as follows (includes the resolution for #330):Change [concepts.lib.compare.equalitycomparable]/p4 as follows:
Change concept
StrictTotallyOrdered
([concepts.lib.compare.stricttotallyordered]) as follows (includes the resolution for #330):Change [concepts.lib.compare.stricttotallyordered]/p1 to be:
Change cross-type concept
StrictTotallyOrdered
([concepts.lib.compare.stricttotallyordered]) as follows (includes the resolution for #330):Change [concepts.lib.compare.stricttotallyordered]/p2 as follows:
Change section "Concept
Relation
" ([concepts.lib.callable.relation]) as follows:Change "Concept
Swappable
" ([concepts.lib.corelang.swappable]) as follows:The text was updated successfully, but these errors were encountered: