String concatenation while incrementing
Asked Answered
I

3

37

This is my code:

$a = 5;
$b = &$a;
echo ++$a.$b++;

Shouldn't it print 66?

Why does it print 76?

Ical answered 19/6, 2013 at 9:56 Comment(13)
Ok... That's a bit weird. Assumed it was just classic pre/post incrementing, but once again, PHP leaves me wondering... wtf? Very interesting question!Staminody
It's a secret attempt to make it harder to write unreadable code.Trumpetweed
What's even more fun is that the concatenation is required. echo ++$a; echo $b++; outputs 66. Oh, PHP... (Edit: seems to be a type-system issue [or maybe operator associativity? or precedence???]. echo ++$a . '' . $b++; gives 66 x.x)Staminody
It should be something with associativity (I know it's left associative) .[dot] operator. When I execute, echo ++$a." ".$b++; it prints 6 6 as expected. but echo ++$a.$b++ changes things. Interesting question :-)Stratocracy
$a = 5; $b = &$a; echo sprintf("%d%d",++$a,$b++); will echo 66, so string concatenation is the problem.Neukam
Want a real mind-@%^!? echo ++$a.++$a.$b++; 777 rather than 677. I suspect we've ventured into the area of undefined behavior. Am combing over operator precedence docs now.Staminody
@Yvette Not true, try it yourself. There is no preceding code necessary to accomplish this issue.Malm
Interesting, echo ++$a."".$b++; will echo 66.Neukam
You can observe the same behavior: $c = 0; echo $c.($c=2); will echo 22. echo $c."".($c=2); will echo 02. So this behavior happended when you cat two number directly, but not with a string in middle.Neukam
it's interesting to observe that, $a = 5; $b = &$a; echo ++$a.$a++; also outputs 76 while it should print 66Stratocracy
@Ical Ok, now we know it's a bug of php, so before php fix it, you can avoid this bug by not using reference, or insert an empty string between two number when doing string concat.Neukam
@Li-chihWu: it's most definitely not a bug in PHP. It's the result of copy-on-write not being triggered in a case where you think it should be triggered. But keep in mind that there's a reason it's triggered (and not). So to call it a bug is a bit of a reach... It's defined and well behaved behavior. Even if it's not what you expect right off the bat. And this is yet another reason I almost never recommend using references...Woodruff
@DCoder it makes writing unreadable code harder, but once you get it, it's even more unreadable than on the beginningBiting
W
66

Alright. This is actually pretty straight forward behavior, and it has to do with how references work in PHP. It is not a bug, but unexpected behavior.

PHP internally uses copy-on-write. Which means that the internal variables are copied when you write to them (so $a = $b; doesn't copy memory until you actually change one of them). With references, it never actually copies. That's important for later.

Let's look at those opcodes:

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 5
   3     1      ASSIGN_REF                                               !1, !0
   4     2      PRE_INC                                          $2      !0
         3      POST_INC                                         ~3      !1
         4      CONCAT                                           ~4      $2, ~3
         5      ECHO                                                     ~4
         6    > RETURN                                                   1

The first two should be pretty easy to understand.

  • ASSIGN - Basically, we're assinging the value of 5 into the compiled variable named !0.
  • ASSIGN_REF - We're creating a reference from !0 to !1 (the direction doesn't matter)

So far, that's straight forward. Now comes the interesting bit:

  • PRE_INC - This is the opcode that actually increments the variable. Of note is that it returns its result into a temporary variable named $2.

So let's look at the source code behind PRE_INC when called with a variable:

static int ZEND_FASTCALL  ZEND_PRE_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_free_op free_op1;
    zval **var_ptr;

    SAVE_OPLINE();
    var_ptr = _get_zval_ptr_ptr_var(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);

    if (IS_VAR == IS_VAR && UNEXPECTED(var_ptr == NULL)) {
        zend_error_noreturn(E_ERROR, "Cannot increment/decrement overloaded objects nor string offsets");
    }
    if (IS_VAR == IS_VAR && UNEXPECTED(*var_ptr == &EG(error_zval))) {
        if (RETURN_VALUE_USED(opline)) {
            PZVAL_LOCK(&EG(uninitialized_zval));
            AI_SET_PTR(&EX_T(opline->result.var), &EG(uninitialized_zval));
        }
        if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
        CHECK_EXCEPTION();
        ZEND_VM_NEXT_OPCODE();
    }

    SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

    if (UNEXPECTED(Z_TYPE_PP(var_ptr) == IS_OBJECT)
       && Z_OBJ_HANDLER_PP(var_ptr, get)
       && Z_OBJ_HANDLER_PP(var_ptr, set)) {
        /* proxy object */
        zval *val = Z_OBJ_HANDLER_PP(var_ptr, get)(*var_ptr TSRMLS_CC);
        Z_ADDREF_P(val);
        fast_increment_function(val);
        Z_OBJ_HANDLER_PP(var_ptr, set)(var_ptr, val TSRMLS_CC);
        zval_ptr_dtor(&val);
    } else {
        fast_increment_function(*var_ptr);
    }

    if (RETURN_VALUE_USED(opline)) {
        PZVAL_LOCK(*var_ptr);
        AI_SET_PTR(&EX_T(opline->result.var), *var_ptr);
    }

    if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

Now I don't expect you to understand what that's doing right away (this is deep engine voodoo), but let's walk through it.

The first two if statements check to see if the variable is "safe" to increment (the first checks to see if it's an overloaded object, the second checks to see if the variable is the special error variable $php_error).

Next is the really interesting bit for us. Since we're modifying the value, it needs to preform copy-on-write. So it calls:

SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

Now, remember, we already set the variable to be a reference above. So the variable is not separated... Which means everything we do to it here will happen to $b as well...

Next, the variable is incremented (fast_increment_function()).

Finally, it sets the result as itself. This is copy-on-write again. It's not returning the value of the operation, but the actual variable. So what PRE_INC returns is still a reference to $a and $b.

  • POST_INC - This behaves similarly to PRE_INC, except for one VERY important fact.

Let's check out the source code again:

static int ZEND_FASTCALL  ZEND_POST_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    retval = &EX_T(opline->result.var).tmp_var;
    ZVAL_COPY_VALUE(retval, *var_ptr);
    zendi_zval_copy_ctor(*retval);

    SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
    fast_increment_function(*var_ptr);
}

This time I cut away all of the non-interesting stuff. So let's look at what it's doing.

First, it gets the return temporary variable (~3 in our code above).

Then it copies the value from its argument (!1 or $b) into the result (and hence the reference is broken).

Then it increments the argument.

Now remember, the argument !1 is the variable $b, which has a reference to !0 ($a) and $2, which if you remember was the result from PRE_INC.

So there you have it. It returns 76 because the reference is maintained in PRE_INC's result.

We can prove this by forcing a copy, by assigning the pre-inc to a temporary variable first (through normal assignment, which will break the reference):

$a = 5;
$b = &$a;
$c = ++$a;
$d = $b++;
echo $c.$d;

Which works as you expected. Proof

And we can reproduce the other behavior (your bug) by introducing a function to maintain the reference:

function &pre_inc(&$a) {
    return ++$a;
}

$a = 5;
$b = &$a;
$c = &pre_inc($a);
$d = $b++;
echo $c.$d;

Which works as you're seeing it (76): Proof

Note: the only reason for the separate function here is that PHP's parser doesn't like $c = &++$a;. So we need to add a level of indirection through the function call to do it...

The reason I don't consider this a bug is that it's how references are supposed to work. Pre-incrementing a referenced variable will return that variable. Even a non-referenced variable should return that variable. It may not be what you expect here, but it works quite well in almost every other case...

The Underlying Point

If you're using references, you're doing it wrong about 99% of the time. So don't use references unless you absolutely need them. PHP is a lot smarter than you may think at memory optimizations. And your use of references really hinders how it can work. So while you think you may be writing smart code, you're really going to be writing less efficient and less friendly code the vast majority of the time...

And if you want to know more about References and how variables work in PHP, checkout One Of My YouTube Videos on the subject...

Woodruff answered 19/6, 2013 at 12:5 Comment(0)
M
0

I think the full concatenate line is first executed and than send with the echo function. By example

$a = 5;
$b = &$a;
echo ++$a.$b++;
// output 76


$a = 5;
$b = &$a;
echo ++$a;
echo $b++;
// output 66

EDIT: Also very important, $b is equal to 7, but echoed before adding:

$a = 5;
$b = &$a;
echo ++$a.$b++; //76
echo $b;
// output 767
Mervin answered 19/6, 2013 at 10:7 Comment(6)
And how does that generate a difference in values since both values should be equal due to =&-operator?Malm
in the second example $a is echoed first. In the next line $b is incremented but doesn't make any difference for the already echoed $aMervin
That's true. But how does it even happen that a 7 exists here?Malm
Because "$a=5; echo ++$a; // 6, echo $a++; // 5". When ++$a the value is incremented and than returned, when $a++ the value before adding 1 is returned. Then in the example of OP I believe $b++ still increments $a which is already incremented ==> 7, but $b will be echoed with the value before adding ==> 6, but afterwards this will also be 7Mervin
not only $b, $a is also 7 in the last line.They share the same memory location (value)Stratocracy
I did another combinations and tried to figure out the solution by that. And i can't :) Here is the demo: eval.inLorineloriner
P
0

EDIT: adding Corbin example: https://eval.in/34067

There's obviously a bug in PHP. If you execute this code:

<?php

{
$a = 5;
echo ++$a.$a++;
}

echo "\n";

{
$a = 5;
$b = &$a;
echo ++$a.$b++;
}

echo "\n";

{
$a = 5;
echo ++$a.$a++;
}

You get:

66 76 76

Which means the same block (1st and 3rd one are identical) of code doesn't always return the same result. Apparently the reference and the increment are putting PHP in a bogus state.

https://eval.in/34023

Papilionaceous answered 19/6, 2013 at 10:28 Comment(16)
Could someone comment the downvote because I'm still trying to figure out why this code gets this result...Papilionaceous
There is something strange. Try to remove the second code block. Or simply add unset($b); right after the second block. If you do that, both 1 and 3 would return 66Lorineloriner
Uuuh yeah... I shouldn't have to do that if there weren't a bug. I'm trying to demonstrate a bug, not to get the code working by all means......Papilionaceous
@Yvette, yes if you execute once. But if you execute the code above, you'll see the result is inconsistent.Papilionaceous
People please try to read and understand the answer before downvoting it.... incredible.Papilionaceous
I'm just pointing out, that simply having a $b referenced to $a changes the result of echo ++$a.$a++Lorineloriner
@Yvette no you did not. You say "{ $a = 5; echo ++$a.$a++; } produces 66" and this is wrong, as I demonstrate above, it does NOT always. So why downvote a documented code?Papilionaceous
@Yvette did you at least click the link to eval.in ?Papilionaceous
+1 Just came to this conclusion myself 17 minutes late. A non-used reference should have no effect on an operation, but sure enough it does. Makes no sense at all. Definitely smells of a bug.Staminody
@Staminody I like it. You refine the problem with the reference never being used.Papilionaceous
@Staminody i added you example above, feel free to remove it if you're not comfortable with that.Papilionaceous
They are not the same block, unset($b) and you will get the same result of first block. It's reference causing the bug.Neukam
@Li-chihWu As you say 'causing' the bug. So there's a bug. So the code shows the bug. So the code is relevant. I'm NOT trying to get it working, I'm trying to show a bug.Papilionaceous
@Papilionaceous Sorry, I know you are demostration the bug, I mean with and without a unset($b) gives you different code block. I think it's me misunderstood your comment, sorry for my poor English.Neukam
@Li-chihWu No pb, and yes you're right the bug definitely comes from the reference.Papilionaceous
Your code proves nothing. PHP does not have block level scoping. So your last example is the same as the second one. Therefore, it is consistent...Woodruff

© 2022 - 2024 — McMap. All rights reserved.