Intuitively, the ++
and --
operators are equivalent to “add 1 to variable” and “subtract 1 from variable”, respectively. As such, it would be expected that $a=$a+1
, $a+=1
, and ++$a
would end up with the same value in $a
; and likewise, $a=$a-1
, $a-=1
, and --$a
. However, because they are implemented separately, this is not always the case. This RFC proposes changes to the handling of null and array values.
Most arithmetic operators, including +
and +=
are implemented for non-numeric types by implicitly casting to integer, then performing the requested operation. However, ++
and --
are implemented separately for each type via a switch statement in the engine; rather than coercing the current value to an integer, the default case in this switch statement is to silently leave the value unchanged.
On top of this, the ++
operator has a specific implementation for null
, but the --
operator does not, leading to an even more surprising inconsistency.
A common concern is that fixing these inconsistencies would imply “endorsing” or even “encouraging” the use of these operators with non-numeric types. It would perhaps be better to make these operations into errors, either immediately in PHP 8.0, or plan ahead to doing so in future. If this is likely to happen, then changing the behaviour in any backwards incompatible way would mean users having to change code twice, which would be undesirable.
The position of this RFC is that arithmetic operators should either be consistently allowed or consistently disallowed on a particular type. In particular, if $a+=1
and $a-=1
do not error, then $a++
and $a--
should not error either.
The balance of these factors is assessed for each type below, but in summary:
--
Strings overload the ++
and --
operators with complex behaviour which mixes text and arithmetic logic. Improving this deserves its own discussion, so will not be discussed further in this RFC.
Adding an integer to an array throws an Error with the message “Unsupported operand types”, as do most other arithmetic operators. However, incrementing or decrementing an array leaves it unchanged, with no Notice, Warning, or Error.
This RFC proposes to change the behaviour so that $a=[]; $a++;
(or ++$a
) and $a=[]; $a--;
(or --$a
) raise the same error as $a = [] + 1;
.
Initial Value | $a = $a + 1 | $a += 1 | ++$a, $a++ | $a = $a - 1 | $a -= 1 | --$a, $a-- | |
---|---|---|---|---|---|---|---|
Current | any array | Error | Error | no effect | Error | Error | no effect |
Proposed | any array | Error | Error | Error | Error | Error | Error |
Most arithmetic operations will implicitly cast non-numeric types to integer as follows:
The ++
and --
operators currently have no implementation for these types, so silently leave the value unchanged. This leads to the following inconsistent behaviour:
Initial Value | $a = $a + 1 | $a += 1 | ++$a, $a++ | $a = $a - 1 | $a -= 1 | --$a, $a-- |
---|---|---|---|---|---|---|
any object | 2 (with E_NOTICE) | 2 (with E_NOTICE) | no effect | 0 (with E_NOTICE) | 0 (with E_NOTICE) | no effect |
resource#1 | 2 | 2 | no effect | 0 | 0 | no effect |
resource#5 | 6 | 6 | no effect | 4 | 4 | no effect |
false | 1 | 1 | false | -1 | -1 | false |
true | 2 | 2 | true | 0 | 0 | true |
Possible options:
++
and --
with an implicit cast, to match other arithmetic operators.++
and --
, but leave other operators unchanged.The position of this RFC is that option A brings very little benefit for these types, because the resulting values aren't particularly intuitive or useful. On the other hand, option B simply swaps one inconsistency for another. Option C has the added benefit of bringing the behaviour of these types in line with the behaviour of arrays.
The implications of introducing such an error deserve their own RFC. For instance, it was mentioned in engine_warnings that the ==
operator currently falls back to an integer cast for cases such as $a == 1
which should not constitute a Type Error.
The position of this RFC is to leave these types unchanged, in the expectation of a further RFC implementing option C.
Nulls deserve special consideration for two reasons:
++
operator already works, leaving just --
as an anomaly
For nearly all arithmetic operators, null
is silently coerced to 0
. Unlike the other types mentioned in this RFC, this applies equally to the ++
operator. Oddly, however, the --
operator is not implemented for null, and leaves the variable unchanged.
This behaviour has been raised as a bug at least three times (#20548, #25674, #41690). All three are closed, and it is documented in the manual, but no evidence has been put forward that this is intentional behaviour, rather than a very old bug retained for compatibility.
Consider the following patch, which looks like a straight-forward simplification:
function foo($bar) { - $bar -= 1; + $bar--; // more code here }
The old code always results in an integer in $bar
after the first line of the function; the new code does not do so if a null value is passed, because of the missing --
implementation.
The asymmetry of ++
and --
is even more confusing. Consider this patch:
function repeatThing($extraTimes) { - for ( $i = 0; $i <= $extraTimes; $i++ ) { + for ( $i = $extraTimes; $i >= 0; $i-- ) { doThing($i); } }
Again, this looks like a safe change, reversing the order of the loop; but if passed a value of NULL, the old code will call doThing() once, and the new code will enter an infinite loop.
There are four main options to improve the situation:
--
with an implicit cast, to match other arithmetic operators including ++
.--
, but leave other operators including ++
unchanged.++
and --
, but leave other operators unchanged.
Option B1 is in some ways the safest, because it makes the change immediately visible, and most cases affected are probably already buggy. However, it is hard to justify raising an error for $a--
without at least a plan to deprecate the obvious counterpart $a++
. A similar argument applies to option B2, which would introduce a breaking change for $a++
, but users could simply switch to $a+=1
and get no error.
Should we then go with Option C, as proposed for other types? That is, should we introduce, either immediately, or via a staged deprecation, a Type Error for arithmetic operations where either argument is null?
An important consideration is that undefined variables, array items, and object properties are currently treated as having a value of null. While it has been suggested to change this to an error in a future version, there is no clear consensus to do so. This means operations on null are more common than the other types we have discussed, and are likely to remain so.
Furthermore, the value when coerced to int is more intuitive, and more likely to be useful. It is fairly reasonable that $price * $_GET['quantity']
will result in zero if no quantity was provided; or that $price -= $discount
leaves the value unchanged if there is no discount defined.
Even the increment operator itself can be useful with null / unitialised values, for instance in this common implementation of “count items in each category”:
$counts = []; foreach ( $items as $item ) { $counts[$item->getCategory()]++; }
If there was no implicit default of zero, an extra line such as $counts[$item->getCategory()] ??= 0;
would need to be added, but this neither expresses the intent (a count of zero is never returned) nor avoids any bugs (we know that the array has been initialised empty).
Since the case for removing null to int coercion is much less decisive than for other types, this RFC makes the assumption that no such removal is likely in the short term. As such, waiting for that removal does not solve the immediate problem of inconsistency between ++
and --
.
As mentioned above, no evidence has been put forward that the missing definition of decrement for null is intentional.
This RFC therefore proposes to fix this bug, so that $a=null; $a--;
or $a=null; --$a;
will result in $a holding -1. This brings it into line with all other mathematical operations, including $a++
, which treat null as equivalent to 0.
Initial Value | $a = $a + 1 | $a += 1 | ++$a, $a++ | $a = $a - 1 | $a -= 1 | --$a, $a-- | |
---|---|---|---|---|---|---|---|
Current | null | 1 | 1 | 1 | -1 | -1 | null |
Proposed | null | 1 | 1 | 1 | -1 | -1 | -1 |
The additional error on increment and decrement for arrays is likely to mostly affect code which already contains bugs, and as a fatal will be easily detected.
The fix to decrement on nulls is harder to detect. While it's unlikely that any code is deliberately relying on the current behaviour, it may inadvertently be doing so. It would be sensible to provide links in official upgrading documentation to static analysis tools which can point out potentially affected code.
8.0
If there is currently any optimisation based on the non-implementation of decrement for nulls, or the behaviour of increment and decrement for arrays, this will need to be amended if the respective votes pass.
None at time of initial discussion.
As mentioned above, this RFC proposed no changes to handling of strings, booleans, objects, or resources.
It also does not directly propose any changes to operators other than ++
and --
, although it recommends that a future RFC does so for the above types.
The two proposed changes can be implemented independently, so two separate votes are proposed, each requiring the standard 2/3 super-majority to be accepted:
$a=null; $a--;
results in -1
? (Yes / No)
None yet. The implementation should be a simple addition to the increment_function
and decrement_function
definitions in Zend/zend_operators.c
.
TODO
A previous version of the RFC proposed fixing the implementation for booleans to match integer coercion followed by adding or substracting 1. The current draft instead groups booleans with objects and resources, as warranting a separate RFC proposing an error.