Programming Languages · Language Design

Short-Circuit Evaluation as a Language Guarantee

How && and || guarantee operand order, why that guarantee is a control-flow construct rather than an optimisation, and what nullable and optional chaining borrow from it.

a && b looks like an ordinary binary operator, but its right operand may never be evaluated. That is not an optimisation the compiler is allowed to make; it is the operator’s definition. The distinction matters because it means && cannot be a function, cannot be overloaded without restriction in some languages, and cannot be reordered by an optimiser. It also means the operator is a control-flow construct wearing operator syntax.

The guarantee and its formulation

In C, a && b first evaluates a. If a compares equal to zero, the result is 0 and b is not evaluated. Otherwise b is evaluated and its comparison to zero determines the result. The || operator is the mirror: if a compares unequal to zero, the result is 1 and b is not evaluated.

The specification states this as behaviour, not as permission. A compiler that evaluated b unconditionally and discarded the result would produce the same value for pure operands but a different program, because b may have side effects or may not terminate. The guarantee is therefore observable, and it is what makes the operators usable for guarding:

if (ptr != NULL && ptr->len > 0) {
    consume(ptr);
}

The second operand dereferences ptr, which would be undefined if ptr were null. The && guarantees the dereference happens only when the first test passed. This idiom is so common that the operator’s short-circuit behaviour is load-bearing for memory safety in C code, and a compiler that broke it would be non-conforming.

The value produced is an int in C, either 0 or 1, not the value of the operand. That detail differs across languages: Python’s and returns one of its operands rather than a boolean, so x and y yields y if x is truthy and x otherwise. JavaScript’s && and || do the same, which is what allows the || default idiom:

const port = configuredPort || 8080

The expression yields 8080 when configuredPort is falsy and the configured value otherwise. Returning an operand rather than a boolean is what makes the idiom work, and it is also what makes the idiom wrong when 0 is a legitimate configured value.

Why it cannot be an ordinary function

A function call evaluates all its arguments before the call. If && were a function, both operands would be evaluated, and the guard would not guard. That is why languages that allow operator overloading either forbid overloading && and || or give the overloaded form different semantics.

C++ permits overloading operator&& and operator|| for class types, and an overloaded operator is an ordinary function call: both operands are evaluated before the call, and the result is whatever the function returns. The built-in guarantee is not preserved, which is why such overloads are avoided for any type whose operands are expensive or effectful. A library that needs the guarded form either provides a named function and documents that it evaluates both arguments, or requires the caller to pass a callable for the second operand so that evaluation can be deferred.

The alternative is a macro. A macro can expand to a conditional expression and preserve the guarantee, at the cost of evaluating its arguments more than once if written carelessly. The operator exists so that the guarded form is a first-class expression rather than a textual expansion, which is also what lets the compiler type-check it.

Languages with lazy evaluation, such as Haskell, get the same effect from a different mechanism: && is defined in terms of pattern matching on the first argument, so the second is not evaluated until the first is known to be true. The guarantee is a consequence of non-strict evaluation rather than of a special operator rule, and it extends to every function, which is a much stronger property than the one C provides.

What the guarantee enables

The guard idiom is the primary use. It appears in pointer checks, bounds checks, and type tests, and in each case the second operand is only meaningful when the first succeeded.

The second use is sequencing of side effects in a condition. open() && read() && close() is a compact way to express a chain of operations that stops at the first failure, and it works because each operand is evaluated only if the previous returned true. The form is terse and it conflates failure with a false value, so it is usually better written as explicit statements; the operator is not a general sequencing construct.

The third use is defaulting, in the languages where the operator returns an operand. a || b yields b when a is falsy, and that is the shortest spelling of a fallback. The same mechanism in reverse, a && b, yields b when a is truthy, which is a conditional execution with a value.

The fourth use is in preconditions of macros and assertions, where the guard prevents an out-of-bounds read in a diagnostic expression. Since assertions are often compiled out, the guarantee also means the guarded expression has no cost when the assertion is disabled, which is a property the macro form could not provide as reliably.

Null-conditional and optional chaining

Several languages added syntax that generalises the guard. C# has ?. for member access that yields null when the receiver is null, and ?? for a null-coalescing fallback. JavaScript has ?. and ??. Kotlin has ?. and the Elvis operator ?:. Swift has ?. and ??.

These forms are short-circuiting in the same sense: a?.b.c does not evaluate .c when a is null, and the whole expression yields null. The chaining propagates through the expression without requiring a test at each step. The mechanism is a conditional evaluation encoded in the member access, and the guarantee is that the right side is not reached when the left side is null.

The design question is what the short-circuit produces. A null result is the common choice, which means the expression’s type must admit null and the caller must handle it. A language with a non-nullable type system can make the operator return an optional type, so the null cannot be ignored without an explicit unwrap. That is the difference between ?. in a language with nullable reference types and the same syntax in one without: the operator is identical, but the type system decides whether the resulting null can escape.

?? is the fallback operator, and it is short-circuiting in the other direction: the right operand is evaluated only when the left is null. The combination a?.b ?? default reads as “the field, or the default if the receiver or the field is null”, and the two operators compose because each guards the next.

The precedence interaction is where these operators get interesting. ?? must bind tighter than assignment and looser than member access, and its position relative to || is a design decision. JavaScript forbids mixing ?? with || or && without parentheses, precisely because the intended grouping is ambiguous and the language chose to require an explicit decision. That is an unusual move: instead of picking a precedence, the grammar rejects the expression. The cost is verbosity in the rare mixed case; the benefit is that no program silently means something other than what it reads like.

The cost of the guarantee

Short-circuiting constrains optimisation in the same way any sequencing guarantee does. A compiler cannot hoist the right operand above the branch, cannot speculate it, and cannot combine two guarded expressions that share a subexpression if the guards differ. The constraint is local and usually cheap, because the right operand is often a simple test.

The guarantee also interacts with purity. In a language with no side effects, short-circuiting is unobservable except for termination, and a compiler could in principle evaluate both operands. In a language with side effects, it is observable and must be preserved. The same syntax therefore has different implementation freedom depending on the surrounding semantics, and a language that adds effects to a previously pure construct has to revisit the rule.

The third cost is diagnostic. A guard like p != NULL && p->len > 0 is checked by the type system only if the language can refine the type of p after the test. In C it cannot, so the dereference is unchecked and the guarantee is a runtime one. In a language with flow-sensitive typing, the same expression can narrow p to a non-null type in the second operand, which turns the runtime guard into a static one. Languages that do this make the operator more useful and the type checker more complex.

The conditional operator ?: is short-circuiting by definition: exactly one of the two branches is evaluated. That is why it can be used to guard an expression that would otherwise be invalid, and why it cannot be replaced by a function taking both branches.

Pattern matching with guards is the same idea in a different syntax: a guard is evaluated only after the pattern matches, and the bindings from the pattern are in scope in the guard. The sequencing is part of the match semantics, and a failed guard falls through to the next arm rather than failing the whole match.

The general principle, that a condition can protect an expression that follows it, is what all of these share. It is a control-flow property expressed in expression syntax, and the reason it must be built into the language rather than provided by a library is that a function cannot defer evaluation of its arguments. The same concern about ordering appears from the opposite direction in Evaluation Order, Sequencing Points, and Unspecified Behaviour, where the question is which order the language guarantees rather than whether an operand runs at all.

The guard also connects to Pattern Matching and the Exhaustiveness Check, where a test that guards a branch is used to refine the type of a value. A language that combines both makes the guard a static obligation, and the short-circuit guarantee is the reason the refinement is sound: the narrowed type holds only on the path where the test succeeded, and the operator is what creates that path.

References

  1. ISO/IEC 9899 — Programming languages — C, 6.5.13 Logical AND operator — ISO/IEC
  2. The Java Language Specification — 15.24 Logical OR operator — Oracle
  3. The Python Language Reference — Boolean operations — Python Software Foundation