Блог пользователя ace5

Автор ace5, 15 месяцев назад, По-английски

Take a look at this code:

Code

At first glance, it should output $$$3$$$ because that's what $$$f()$$$ returns, which is then assigned to $$$k_0$$$. But when executed, this code outputs $$$1$$$!

This code is very short and everything seems clear, we're simply assinging $$$k_0$$$ to the return value of $$$f()$$$ which is $$$3$$$, and the push_back in $$$f()$$$ clearly doesn't affect the result... or does it?

How an assingment operation really works

It may seem obvious at first, we just copying one value to another, but let's see how compiler actually handles this operation.

Consider this code. (Source)

Code

This code can output any permutation from '$$$abcabc$$$' to '$$$cbacba$$$', because the evaluation order of function arguments and operands in addition is unsequenced in C++. This flexibility allows compilers to optimize performance by evaluationg arguments in any order (or even simultaneously).

And assignment is just another operation, meaning it's left and right operands can be evaluated in any order.

In our original example here's what happens:

1) Compiler evaluates the left operand (obtaining a reference to $$$k_0$$$).

2) Then, it calls $$$f()$$$.

3) Finally, it assigns the return value ($$$3$$$) to the previously obtained reference.

And if you're familiar with how vectors work, you might already see the issue.

How vectors work

A vector is a dynamic container, it's size can change, and elements can be added ('push_back') or removed ('pop_back').

Every vector has a capacity (the allocated memory space, initially empty). And when you call push_back, one of two things happens:

1) Capacity is sufficient. The new element is placed at the end, and the vector's 'end()' pointer is incremented.

2) Capacity is insufficient. A new memory block (typically $$$2x$$$ the current size) is allocated, existing elements are copied/moved to the new location, and then everything is the same as in the first case.

Vectors store elements contiguosly for fast indexing, so if reallocation occurs, the entire vector moves to a different memory location.

So what happens?

1) k.push_back(1) initializes the vector with $$$1$$$.

2) In $$$k_0 = f()$$$, the compiler:

2.1) First evaluates the left side (obtaining a reference to $$$k_0$$$).

2.2) Then calls $$$f()$$$, which performs k.push_back(3), and if reallocation occurs, reference to $$$k_0$$$ changes.

2.3) Finally, it assigns $$$3$$$ to the now invalid reference, leaving the actual $$$k_0$$$ unchanged ($$$1$$$).

Conclusion

You can avoid this bug in many different ways, such as:

1) Storing the value of $$$f()$$$ in a variable and then assigning it to $$$k_0$$$

2) Preallocate memory (do k.reserve(2) in this example) to ensure no reallocation happens during push_back.

3) Push_back to the vector before or after the function if possible.

I faced this mistake while debugging a Cartesian tree problem, and spent 2-3 hours figuring it out. Hopefully, this blog helps you avoid the same mistake or at least know how to fix it.

Have you faced a similar mistake?

  • Проголосовать: нравится
  • +254
  • Проголосовать: не нравится

»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится +28 Проголосовать: не нравится

Compiling the first code in codeforces custom test, results in 3 as the output

I also ran it with my own system, still 3

»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

o_0

»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится -18 Проголосовать: не нравится

Another solution is to use Rust (similar code is not compiled)

»
15 месяцев назад, скрыть # |
← Rev. 2  
Проголосовать: нравится 0 Проголосовать: не нравится

A similar issue can also occur when using push_back in a range-based for loop:

Example
»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится +1 Проголосовать: не нравится

I've had this bug during a live contest; SWERC 2021-2022, problem C. You can see the frustration from the scoreboard.

I honestly have no clue how I was able to debug this in a 200+ line code, in the last hour (I didn't do the intended simpler solution). But it taught me a lesson for sure.

I hope it didn't ruin your performence in a live contest!

»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится -8 Проголосовать: не нравится

another faulty one with the same logic:

vector<int> a(n);
int &x = a[0];
a.push_back(42);
cout << x;
»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

Used a pointer tree* to a vector,

forgot to do reserve, passes public/open test case, always fail at closed test case.

Wasted around 30 mins debugging.

»
15 месяцев назад, скрыть # |
 
Проголосовать: нравится +20 Проголосовать: не нравится

"And assignment is just another operation, meaning it's left and right operands can be evaluated in any order." I would like to point out that this is no longer true since C++17.

§[expr.ass]¶1 from the latest C++17 draft:

The right operand is sequenced before the left operand.

It can also be found in rule 19 in the link mentioned in the blog.

This means things like k[0] = f(); (from the blog) or map[x] = map.size(); (something I use often) can be used safely if the online judge uses C++17 or later. If it is the case then I suggest updating the local compilation setup to match the judge to prevent issues like this.