[P1934R0] proposes removing the
boolean concept and replacing its uses with
convertible_to<bool> instead. The proposed replacement fails to completely fix the problems with
boolean identified by the paper, contradicts LWG’s preferred direction for [LWG2114], conflicts with existing practice, imposes an unreasonable burden on implementers and users alike, and should be reconsidered.
[P1934R0] proposes removing the
boolean concept and replacing its uses with
convertible_to<bool> instead. As
boolean is currently used in the comparison concepts and the
predicate concept, this change requires generic code performing comparisons and invoking predicates to convert the result to
bool. This can be done implicitly, explicitly, or contextually:
However, even though the built-in operators
|| contextually convert their operands to
bool, the existence of operator overloading means that they cannot be used directly with something that’s only required to model
convertible_to<bool> is required:
Cast not required
There are a multitude of problems with this proposed replacement.
The keenly-eyed reader may have noticed that the examples in the above table are the same as those in section 2.2 “Nonuniformity of application” of [P1934R0], only modified to be correct.
In other words, P1934R0 fails to solve the motivating example it presented for this problem: a cast is still sometimes required, sometimes not. While it is certainly an improvement over
boolean - the demarcation of the two “sometimes” is cleaner and easier to reason about - the problem remains.
Since C++98, the current library specification has been filled with requirements for something to be “convertible to
bool” requirements (see, e.g., what is now called Cpp17LessThanComparable 18.104.22.168 [utility.arg.requirements]) and “contextually convertible to
bool” has been added to the mix since C++11 (see, e.g., Cpp17NullablePointer 22.214.171.124 [nullablepointer.requirements]).
However, these requirements date from a time where library specification has been quite imprecise; as Casey Carter put it during the LEWG discussion of an early draft of this paper, what was meant was more like “it converts to
bool when the library wants it to convert to
A library issue, [LWG2114], was opened in 2011 to address the formulation of these requirements; in 2012, STL explained in the issue that implementations want to do things well beyond just converting them to
bool; the example given from the Dinkumware Standard Library implementation then were:
All but the first would have required explicit
bool casts if “convertible to
bool” were the sole requirement.
LWG’s direction for LWG2114 has consistently been to require the logical operators to work correctly for these types. However, formulation of correct wording has proven difficult, and since this was seen as a defect in wording and required no implementation changes, it justifiably received a lower priority.
[P1934R0] contradicts this longstanding direction. If the former is adopted, then either we need to require implementations to litter
bool casts everywhere through their existing code, or introduce an odd inconsistency in how the algorithms handle such types. Neither is appealing.
[P1934R0] claims that
convertible_to<bool> “is quite close to the ‘old’ notion of what a predicate should yield”. As seen from the history above, this is simply not the case in practice. None of the three major implementations in fact accept everything “convertible to
bool”; they all make use of logical operations on the result and expect them to work. It takes five minutes to find examples in their current code base:
It’s also worth noting that two of the three examples above depend on the built-in operator
&&’s short-circuiting behavior.
In other words, while “convertible to
bool” or “contextually convertible to
bool” might have been the requirement on paper, it has never been the reality.
The benefit conferred by the choice of
convertible_to<bool> is that it enables certain highly questionable types to be returned from predicates and comparisons - overloading
operator|| is universally recommended against due to such overloads not having the built-in operator’s short-circuit semantics. But the costs imposed are substantial enough to be unreasonable when measured against the minimal benefit it confers:
boolcasts for correctness. The fact that these casts are only sometimes required compounds the problem.
filesystem::directory_iterator. Without further changes in the library specification, users would be required to cast the result of comparing two
bool, which is obviously untenable. Similarly, authors of concrete iterators will need to revise their documentation to assure their users that casting is not required. While this might not be a major burden, the fact that doing so is necessary at all should give us pause.
convertible_to<bool>, we propose to replace
boolean with an exposition-only concept
and further add the semantic requirement that all logical operators “just work”:
!can be overloaded but must have the usual semantics;
||must have their built-in meaning.
This is the same set of operations required to be supported by the current proposed resolution of [LWG2114]; casting to
bool will not be necessary for these operations.
The primary difficulty that has plagued previous attempts at expressing the requirement as to
|| is that it seems to require universal quantification: for a
boolean-testable type, we must require that
|| can be used with every
boolean-testable type. This is impossible to express in concepts, and exceedingly difficult at best to formulate even in prose, as the drafting history of [LWG2114] attests. Moreover, stating the requirement in this manner makes it difficult to answer even simple questions like “is
boolean-testable?” If there’s a
Evil type that works with itself but no other type, is
Evil the broken one, or is it everything else? How can you tell?
To address this difficulty and allow the type to be analyzed in isolation, we propose to strengthen the requirement: the type must not introduce a potentially viable
operator|| candidate into the overload set. This is a requirement that can be answered easily: given a type, we know its member functions and its associated namespaces and classes (if any). We therefore know the result of class member lookup and argument-dependent lookup for the names
operator||. If there is no member with these names, and ADL also does not find an overload that can possibly be viable, then we know that using this type in an expression cannot possibly bring in viable
operator|| overloads, and so using
|| with two such types will always resolve to the built-in operator.
While this is a stronger requirement than what is strictly necessary, it allows analysis based on a single type (possibly by a static analyzer), and still admits a wide set of models including well-behaved class and enumeration types. There is some subtlety here (the wording needs to be carefully crafted to ensure that
operator|| do not disqualify
std::true_type, for example), but the rule to teach is simple: just don’t overload the conditional operators, and your type will be fine.
Alternative suggestions have been made to limit these expression to a small set of permitted types that we know to be well-behaved. Various options have been proposed in this direction.
Permitted result type of a comparison of predicate
||Anything that decays to
||Anything that decays to an integral type, such as
||Anything that decays to an integral/pointer/pointer to member type. This enables returning possibly-null pointers from predicates without having to convert them to
||This accepts two known-good class types for which support have been requested.|
By necessity, they are all significantly more limiting than the
boolean-testable approach: class types and enumeration types cannot be supported generally; even supporting a select set requires giving up support for pointers and pointers to member. This paper does not propose this approach, but mentions it for completeness.