All design processes result in compromises between mutually incompatible goals. Unfortunately, the design process for the overloaded &&
operator in C++ produced a confusing end result: that the very feature you want from &&
-- its short-circuiting behavior -- is omitted.
The details of how that design process ended up in this unfortunate place, those I don't know. It is however relevant to see how a later design process took this unpleasant outcome into account. In C#, the overloaded &&
operator is short circuiting. How did the designers of C# achieve that?
One of the other answers suggests "lambda lifting". That is:
A && B
could be realized as something morally equivalent to:
operator_&& ( A, ()=> B )
where the second argument uses some mechanism for lazy evaluation so that when evaluated, the side effects and value of the expression are produced. The implementation of the overloaded operator would only do the lazy evaluation when necessary.
This is not what the C# design team did. (Aside: though lambda lifting is what I did when it came time to do expression tree representation of the ??
operator, which requires certain conversion operations to be performed lazily. Describing that in detail would however be a major digression. Suffice to say: lambda lifting works but is sufficiently heavyweight that we wished to avoid it.)
Rather, the C# solution breaks the problem down into two separate problems:
- should we evaluate the right-hand operand?
- if the answer to the above was "yes", then how do we combine the two operands?
Therefore the problem is solved by making it illegal to overload &&
directly. Rather, in C# you must overload two operators, each of which answers one of those two questions.
class C
{
// Is this thing "false-ish"? If yes, we can skip computing the right
// hand size of an &&
public static bool operator false (C c) { whatever }
// If we didn't skip the RHS, how do we combine them?
public static C operator & (C left, C right) { whatever }
...
(Aside: actually, three. C# requires that if operator false
is provided then operator true
must also be provided, which answers the question: is this thing "true-ish?". Typically there would be no reason to provide only one such operator so C# requires both.)
Consider a statement of the form:
C cresult = cleft && cright;
The compiler generates code for this as thought you had written this pseudo-C#:
C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);
As you can see, the left hand side is always evaluated. If it is determined to be "false-ish" then it is the result. Otherwise, the right hand side is evaluated, and the eager user-defined operator &
is invoked.
The ||
operator is defined in the analogous way, as an invocation of operator true and the eager |
operator:
cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);
By defining all four operators -- true
, false
, &
and |
-- C# allows you to not only say cleft && cright
but also non-short-circuiting cleft & cright
, and also if (cleft) if (cright) ...
, and c ? consequence : alternative
and while(c)
, and so on.
Now, I said that all design processes are the result of compromise. Here the C# language designers managed to get short-circuiting &&
and ||
right, but doing so requires overloading four operators instead of two, which some people find confusing. The operator true/false feature is one of the least well understood features in C#. The goal of having a sensible and straightforward language that is familiar to C++ users was opposed by the desires to have short circuiting and the desire to not implement lambda lifting or other forms of lazy evaluation. I think that was a reasonable compromise position, but it is important to realize that it is a compromise position. Just a different compromise position than the designers of C++ landed on.
If the subject of language design for such operators interests you, consider reading my series on why C# does not define these operators on nullable Booleans:
http://ericlippert.com/2012/03/26/null-is-not-false-part-one/
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
– Lupine{true, false, nil}
. Sincenil&& x == nil
it could short-circuit. – Laudstd::valarray<bool> a, b, c;
, how do you imaginea || b || c
be short-circuited ? – Harewoodoperator&&
oroperator||
and depends on both operands being evaluated. Maintaining backward compatibility is (or should be) important when adding features to an existing language. – Fibroblast