Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a rational explanation for this PHP call-by-value behavior? Or PHP bug?

PHP 5.5.12. Consider this:

<?php
$a = [ 'a', 'b', 'c' ];
foreach($a as &$x) {
    $x .= 'q';
}
print_r($a);

This, as expected, outputs:

Array
(
    [0] => aq
    [1] => bq
    [2] => cq
)

Now consider:

<?php
$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    return $a;
}

This outputs:

Array
(
    [0] => aq
    [1] => bq
    [2] => cq
)

(!) But wait a minute. $a is not being passed by reference. Which means I should be getting a copy back from z(), which would be modified, and $a ought to be left alone.

But what happens when we force PHP to do its copy-on-write magic:

$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    $a[0] .= 'x';
    return $a;
}

For this, we get what I would expect:

Array
(
    [0] => a
    [1] => b
    [2] => c
)

EDIT: One more example...

$a = [ 'a', 'b', 'c' ];
$b = z($a);
foreach($b as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    return $a;
}

This works as expected:

Array
(
    [0] => a
    [1] => b
    [2] => c
)

Is there a rational explanation for this?

like image 934
Nairebis Avatar asked Jul 16 '14 05:07

Nairebis


1 Answers

Update

Bug 67633 has been opened to address this issue. The behaviour has been changed by this commit in an effort to remove reference restrictions from foreach.


From this 3v4l output you can clearly see that this behaviour has changed over time:

Update 2

Fixed with this commit; this will become available in 5.5.18 and 5.6.2.

PHP 5.4

Prior to PHP 5.5 your code would actually raise a fatal error:

Fatal error: Cannot create references to elements of a temporary array expression

PHP 5.5 - 5.6

These versions do not perform copy-on-write when the function result is used directly inside the foreach block. As such, the original array is now used and changes to the elements are permanent.

I personally feel that this is a bug; copy-on-write should have taken place.

PHP > 5.6

In the phpng branch, which is likely to become the basis of a next major version, constant arrays are made immutable so the copy-on-write is correctly performed only in this case. Declaring the array like below will exhibit the same issue with phpng:

$foo = 'b';
$a = ['a', $foo, 'b'];

Proof

Hack (HHVM)

Only Hack handles the situation correctly as it currently stands.

The right way

The documented way of using the function result by reference is this:

$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

// indicate that this function returns by reference 
// and its argument must be a reference too
function &z(&$a)
{
    return $a;
}

Demo

Other fixes

To avoid changing the original array, for now, you have the following options:

  1. Assign the function result into a temporary variable before the foreach;
  2. Don't use references;
  3. Switch to Hack.
like image 53
Ja͢ck Avatar answered Nov 15 '22 15:11

Ja͢ck