boolean-testable
Document #: | P1964R1 |
Date: | 2020-01-11 |
Project: | Programming Language C++ LWG |
Reply-to: |
Tim Song <t.canens.cpp@gmail.com> |
This paper provides wording for replacing boolean
with the boolean-testable
exposition-only concept, as proposed in [P1964R0]. For detailed motivation and discussion, see that paper.
An early draft of [P1964R0] was presented to LEWG on Friday afternoon in Belfast. The following polls were taken:
Introduce an exposition-only concept
$NAME
as a replacement for the existing use of theboolean
concept.
- subsume
convertible_to
- add syntactic checking for
!
- add semantic requirements for “there is no
operator||
noroperator&&
that can be found in any use of T”In favor of the above design.
SF F N A SA3 11 2 0 0 Name is
boolean-testable
Unanimous consent
Tim and Casey will update P1964 with wording for the above design. We forward that paper to LWG for C++20.
Unanimous consent
Yes, this is a lot of words to specify what is basically “don’t be dumb”. Defining “dumb” with sufficient precision turns out to be complicated.
The basic requirement is that a boolean-testable
type must not contribute any potentially viable operator&&
or operator||
overload to the overload set. If so, then any two boolean-testable
types can be freely used with these operators, because there are no viable user-defined overloads and therefore they must have their built-in meaning.
There are three cases to consider:
operator&&
or operator||
, period.There are some wrinkles for this third case.
Consider the following example:
namespace X {
enum A {};
template<class>
struct trait { using type = A; };
template<class T>
void operator&&(T*, typename trait<T>::type);
}
This operator&&
will be picked up in an expression like (int*) nullptr && X::A()
. Since int*
should model boolean-testable
(and certainly isn’t responsible for this operator), we must make X::A
not model boolean-testable
. In other words, a non-deduced context, which can encode an arbitrary type transformation, must be considered to match everything.
std::valarray
As mentioned in [P1964R0], the wording needs to avoid catching overloads like std::valarray
’s operator&&
on unsuspecting types in namespace std
. That is, we want declarations like
template<class T>
valarray<bool> operator&&(const valarray<T>&, const typename valarray<T>::value_type&);
and its pre-[LWG3074] form
to disqualify specializations of std::valarray
(or any derived class that might have added a conversion to bool
), but not std::true_type
.
The distinguishing characteristics of these overloads are
std::valarray
);For these overloads, we can safely only consider the parameter(s) satisfying #2 above (called key parameters in the wording below). This is because for template argument deduction to succeed on such a parameter, the type of the provided argument must be either a specialization of that class template or derived from it. If so, then that argument must necessarily also bring this function template into the overload set. As long as the wording excludes such arguments, then, there is no need to worry about other types that may happen to belong in the same namespace.
I have color-coded the two steps in this reasoning because there are corner cases involving each, which I’ll now discuss.
Consider this example:
namespace Y {
template<class>
struct C {};
template<class T>
void operator&&(C<T> x, T y);
template<class T>
void operator||(C<std::decay_t<T>> x, T y);
enum A {};
}
struct B { template<class T> operator T(); };
We don’t want the operator&&
declaration to disqualify Y::A
; however the expression ::B() || Y::A()
will use the Y::operator||
overload, and therefore Y::A
cannot be boolean-testable
, even though its declaration might be superficially similar. The key difference here is that C<std::decay_t<T>>
doesn’t contain anything that participates in template argument deduction, and therefore no longer requires the argument to have any relation to C
, contrary to the first sentence of the reasoning above.
To qualify as a key parameter, then, the type must contain at least one template parameter that participates in template argument deduction.
Hidden friends strike at the second sentence of the reasoning above. Consider:
namespace Z {
template<class>
struct A {
operator bool();
};
struct B {
operator bool();
template<class T>
friend void operator&&(A<T>, B);
};
}
Z::A<int>() && Z::B()
will use the operator&&
overload, but ADL for Z::A<int>()
alone will not even find the hidden friend overload (indeed, the author of Z::A
might have nothing to do with it). So we must disqualify Z::B
instead.
The problem here is that the hidden friend can be a friend of the wrong class. That means that the second sentence of the reasoning above no longer applies, because we are no longer guaranteed that the argument related to A
will bring the overload in.
The wording below excludes hidden friends from the key parameter special case: hidden friends disqualify a type from modeling boolean-testable
if template argument deduction for either parameter succeeds. Note that the concern motivating this special case doesn’t apply to hidden friends: if they are the hidden friend of the “right” class template, then ADL for other types in the namespace will not even find them, so they will not accidentally disqualify anything.
This wording is relative to [N4842].
Replace 18.5.2 [concept.boolean] with the following:
1 The exposition-only boolean-testable
concept specifies the requirements on expressions that are convertible to bool
and for which the logical operators (7.6.14 [expr.log.and], 7.6.15 [expr.log.or], 7.6.2.1 [expr.unary.op]) have the conventional semantics.
2 Let e
be an expression such that decltype((e))
is T
. T
models boolean-testable-impl
only if:
remove_cvref_t<T>
is not a class type, or name lookup for the names operator&&
and operator||
within the scope of remove_cvref_t<T>
as if by class member access lookup (11.8 [class.member.lookup]) results in an empty declaration set; andoperator&&
and operator||
in the associated namespaces and entities of T
(6.5.2 [basic.lookup.argdep]) finds no disqualifying declaration (defined below).3 A disqualifying parameter is a function parameter whose declared type P
e
to P
; orP
contains no template parameter that participates in template argument deduction (13.10.2.5 [temp.deduct.type]), ore
as the argument type succeeds.4 A key parameter of a function template D
is a function parameter of type cv
X
or reference thereto, where X
names a specialization of a class template that is a member of the same namespace as D
, and X
contains at least one template parameter that participates in template argument deduction.
[ Example: In
namespace Z {
template<class>
struct C {};
template<class T>
void operator&&(C<T> x, T y);
template<class T>
void operator||(C<type_identity_t<T>> x, T y);
}
the declaration of Z::operator&&
contains one key parameter, C<T> x
, and the declaration of Z::operator||
contains no key parameters. — end example ]
5 A disqualifying declaration is
6 [ Note: The intention is to ensure that given two types T1
and T2
that each model boolean-testable-impl
, the &&
and ||
operators within the expressions declval<T1>() && declval<T2>()
and declval<T1>() || declval<T2>()
resolve to the corresponding built-in operators. — end note ]
7 Let e
be an expression such that decltype((e))
is T
. T
models boolean-testable
only if bool(e) == !bool(!e)
.
8 [ Example: The types bool
, true_type
(20.15.2 [meta.type.synop]), int*
, and bitset<N>::reference
(20.9.2 [template.bitset]) model boolean-testable
. — end example ]
Edit 18.3 [concepts.syn] as indicated:
Replace all instances of boolean
in 17.11.4 [cmp.concept], 18.5.3 [concept.equalitycomparable], 18.5.4 [concept.totallyordered] and 18.7.4 [concept.predicate] with boolean-testable
.
Edit 16.4.2.1 [expos.only.func] as indicated:
constexpr auto synth-three-way = []<class T, class U>(const T& t, const U& u) requires requires { - { t < u } -> convertible_to<bool>; - { u < t } -> convertible_to<bool>; + { t < u } -> boolean-testable; + { u < t } -> boolean-testable; } { if constexpr (three_way_comparable_with<T, U>) { return t <=> u; } else { if (t < u) return weak_ordering::less; if (u < t) return weak_ordering::greater; return weak_ordering::equivalent; } };
Edit 25.5.5 [alg.find] p1 as indicated:
1 Let E be:
- (1.1)
*i == value
forfind
,- (1.2)
pred(*i) != false
forfind_if
,- (1.3)
pred(*i) == false
forfind_if_not
,- (1.4)
bool(
invoke(proj, *i) == value
)
for ranges::find,- (1.5)
bool(
invoke(pred, invoke(proj, *i))
)
!= false
forranges::find_if
,- (1.6)
bool(!
invoke(pred, invoke(proj, *i))
)
== false
forranges::find_if_not
.
Edit 25.5.7 [alg.find.first.of] p1 as indicated:
1 Let E be:
Edit 25.5.8 [alg.adjacent.find] p1 as indicated:
1 Let E be:
Edit 25.5.9 [alg.count] p1 as indicated:
1 Let E be:
- (1.1)
*i == value
for the overloads with no parameterpred
orproj
,- (1.2)
pred(*i) != false
for the overloads with a parameterpred
but no parameterproj
,- (1.3)
invoke(proj, *i) == value
for the overloads with a parameterproj
but no parameterpred
,- (1.4)
bool(
invoke(pred, invoke(proj, *i))
)
!= false
for the overloads with both parametersproj
andpred
.
[LWG3074] Jonathan Wakely. Non-member functions for valarray should only deduce from the valarray.
https://wg21.link/lwg3074
[N4842] Richard Smith. 2019. Working Draft, Standard for Programming Language C++.
https://wg21.link/n4842
[P1964R0] Tim Song. 2019. Casting convertible_to<bool> considered harmful.
https://wg21.link/p1964r0