13

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?

Nairebis
  • 283
  • 1
  • 8
  • 1
    Looks to me like `$a` **is** being passed by reference in example #2. This is indeed different to what happens in PHP 5.4 ~ https://eval.in/172839. Might have something to do with the array dereferencing changes introduced in 5.5 – Phil Jul 16 '14 at 05:48
  • Your last example is invalid. `$b = z($a);` is being applied to the whole array at once. – Hanky Panky Jul 16 '14 at 05:49
  • 1
    From [these results](http://3v4l.org/MIA2p) it should be obvious that you shouldn't actually rely on this behaviour; it could be a bug in fact. – Ja͢ck Jul 16 '14 at 06:16
  • Got another whacky example for you ~ https://eval.in/172857 – Phil Jul 16 '14 at 06:24
  • To me, it's not **array** passed by reference to foreach, but **array elements** (one-by-one). From this point, all examples are correct. – Michael Livach Jul 16 '14 at 07:05
  • @MichaelLivach Based on that, shouldn't the third example have been `['axq', 'bq', 'cq']` then? – Ja͢ck Jul 16 '14 at 07:23
  • @Jack No, $a was copied with all elements here: `$b = z($a);` – Michael Livach Jul 16 '14 at 08:07
  • @MichaelLivach The one with `$b` in it is the fourth example. – Ja͢ck Jul 16 '14 at 08:09
  • tried this: ` – Michael Livach Jul 16 '14 at 08:09
  • @Jack, third - with `$a[0] .= 'x';` ? It's correct too: copy-on-write did full copy of $a$ the copy was lost after foreach() finished – Michael Livach Jul 16 '14 at 08:18
  • @Jack, look at this example, please: ` $a = [ 'a', 'b', 'c' ]; $b = [ &$a[0], &$a[1], &$a[2] ]; foreach(z($b) as &$x) { $x .= 'q'; } print_r($a); print_r($b); function z($v) { $v[0] .= 'x'; $v[3] = 'woo'; return $v; } ` – Michael Livach Jul 16 '14 at 08:26
  • @MichaelLivach I don't see why you seem to think that such an example proves that this seems to be correct behaviour; you can clearly see from my earlier 3v4l comment that it doesn't work reliably across different versions of php. – Ja͢ck Jul 16 '14 at 08:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/57387/discussion-between-michael-livach-and-jack). – Michael Livach Jul 16 '14 at 09:46
  • Thank you everyone for your input. I agree that it seems to be a bug, but I wanted to be sure. PHP has fooled me before with odd semantics that was actually correct, documented behavior. :) I'll submit a bug report. – Nairebis Jul 16 '14 at 13:38
  • 2
    @Nairebis Just had a quick chat with another php-src developer and he agreed that it's a bug. – Ja͢ck Jul 16 '14 at 14:41
  • Reported as https://bugs.php.net/bug.php?id=67633 – Nairebis Jul 16 '14 at 15:16

2 Answers2

9

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.
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
-1

In this example, the function z does nothing. It doesn't copy or clone anything therefore the response from z() will be the same as not calling at all. You are simply returning the object passed in and therefore the response is as expected.

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

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

Thiis is easier to demonstrate using objects as they are given a system ID:

<?php
$obj = new stdClass();
$obj->name = 'foo';

function z($a)
{
    $a->name = 'bar';
    return $a;
}

var_dump($obj);
var_dump(z($obj));

The output for this is:

object(stdClass)#1 (1) {
  ["name"]=>
  string(3) "foo"
}
object(stdClass)#1 (1) {
  ["name"]=>
  string(3) "bar"
}

Both objects have the ID as "1" which shows they are not copies or clones.

t j
  • 7,026
  • 12
  • 46
  • 66
  • 1
    But in PHP, objects are implicitly passed by reference whereas arrays are not (meant to be) so your example is not accurate. You may be on to something with *"the function z does nothing"* though. Could be simply a compiler optimisation at work – Phil Jul 16 '14 at 06:19
  • @Phil I agree with Phil that the observed behaviour could be the result of an optimisation that, imho, shouldn't work in this way. – Ja͢ck Jul 16 '14 at 06:22
  • You are passing and returning references to objects, not objects themselves. The reference is indeed copied. – newacct Jul 16 '14 at 22:44