Is this code well-defined regardless of copy elision?
Asked Answered
B

5

8

Consider this code:

#include <iostream>

struct Test
{
    int x;
    int y;
};

Test func(const Test& in)
{
    Test out;
    out.x=in.y;
    out.y=in.x;
    return out;
}

int main()
{
    Test test{1,2};
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    test=func(test);
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
}

One would expect an output like this:

x: 1, y: 2
x: 2, y: 1

and this is indeed what I get. But due to copy elision, could out be in the same place in memory as in and result in the last line of output being x: 2, y: 2?

I've tried compiling with gcc and clang with both -O0 and -O3, and the results still look as intended.

Brambling answered 23/10, 2015 at 14:48 Comment(0)
M
3

This is well formed code, optimizations can not break well formed code since it would violate the as-if rule. Which tells us that:

In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below

with an exception for copy elision:

[...]an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.[...]

But sequencing rules still must be followed and if we go to the draft standard we see we know that the assignment is sequenced after the left and right operands are evaluated from section 5.17:

In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression

and we know that the body of the function is indeterminately sequenced with respect to other evaluations not specifically sequenced with the function call from section 1.9:

Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function 9

and indeterminately sequenced means:

Evaluations A and B are indeterminately sequenced when either A is sequenced before B or B is sequenced before A, but it is unspecified which. [ Note: Indeterminately sequenced evaluations cannot overlap, but either could be executed first. —end note ]

Moguel answered 23/10, 2015 at 15:0 Comment(0)
A
4

No, it can not. Optimizations can not break well-formed code, and this code is well-formed.

EDIT: Small update. Of course, my answer assumes the compiler itself is bug-free, which of course is something you can only pray for :)

EDIT2: Some people are talking about side-effects in copy constructors and that they are bad. Of course, they are not bad. They way I see it, is that in C++ you are not guaranteed to have a known number of temporary objects created. You are guaranteed that every temporary object created will be destroyed. While optimizations are allowed to reduce the number of temporary objects by doing copy elision, they are also allowed to increase it! :) As long as your side-effects are coded with this fact in mind, you are good.

Asuncionasunder answered 23/10, 2015 at 14:50 Comment(2)
Is it explicitly allowed by the standard to make extra copies, so that this would introduce unexpected additional side effects from copy constructor? For copy elision indeed there's a special exception, but for "copy multiplication" I've not heard of any.Brambling
I am no standard guru, so I am not sure if there is something which can be construed to prevent compilers from throwing in extra copies.Asuncionasunder
M
3

No, it couldn't!

Optimization doesn't mean that you get undefined behaviour in a well-written (not ill-conditioned) code.

Check this ref:

conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below. ...

A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible execution sequences of the corresponding instance of the abstract machine with the same program and the same input. ...

The observable behavior of the abstract machine is its sequence of reads and writes to volatile data and calls to library I/O functions. ...

taken from this answer.

In this answer, you can see a case where copy-elision may produce different output!

Mccorkle answered 23/10, 2015 at 14:52 Comment(0)
M
3

This is well formed code, optimizations can not break well formed code since it would violate the as-if rule. Which tells us that:

In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below

with an exception for copy elision:

[...]an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.[...]

But sequencing rules still must be followed and if we go to the draft standard we see we know that the assignment is sequenced after the left and right operands are evaluated from section 5.17:

In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression

and we know that the body of the function is indeterminately sequenced with respect to other evaluations not specifically sequenced with the function call from section 1.9:

Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function 9

and indeterminately sequenced means:

Evaluations A and B are indeterminately sequenced when either A is sequenced before B or B is sequenced before A, but it is unspecified which. [ Note: Indeterminately sequenced evaluations cannot overlap, but either could be executed first. —end note ]

Moguel answered 23/10, 2015 at 15:0 Comment(0)
A
2

The only thing which copy elision is allowed to "break" is when you have side effects in your copy constructor. This is not a problem, because copy constructors should always be free of side effects.

Just for illustration, here is a copy constructor with side effects. The behaviour of this program does indeed depend on compiler optimisations, i.e. whether a copy is actually made or not:

#include <iostream>

int global = 0;

struct Test
{
    int x;
    int y;

    Test() : x(0), y(0) {}

    Test(Test const& other) :
        x(other.x),
        y(other.y)
    {
        global = 1; // side effect in a copy constructor, very bad!
    }
};

Test func(const Test& in)
{
    Test out;
    out.x=in.y;
    out.y=in.x;
    return out;
}

int main()
{
    Test test;
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    test=func(test);
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    std::cout << global << "\n"; // output depends on optimisation
}

The code you have shown is free of such side effects, and the behaviour of your program is well-defined.

Adjective answered 23/10, 2015 at 14:58 Comment(14)
Very good answer, since it exploits the one that I have linked to!Mccorkle
What a nonsense! 'side effect in a copy constructor, very bad!' - just called the whole class of shared_ptr very bad...Asuncionasunder
@SergeyA: So why is it nonsense and what does it have to do with shared_ptr?Adjective
Because copy-ctors of shared_ptr, of course, do have side effects.Asuncionasunder
@SergeyA: If you mean incrementing the owner count, that's not really a side effect but belongs to the copy constructor's core functionality. When the copy of a shared_ptr is elided, no copy is made and the counter is not incremented.Adjective
You need to get your head straight. Incrementing a counter is a side-effect of copy-construction and semantically no different from your example. The counter does not even belong to the object being constructed! Bottom line - you are wrong on substance. There is nothing wrong with side effects in copy construction.Asuncionasunder
@SergeyA: Let's put it this way: depending on a copy constructor's side effects is bad, because you can never assume that the copy actually took place. For example, in the code I posted, taking global == 1 as an indicator that func has returned would be a bug. P.S.: You would be well-advised to refrain from this hostile tone here, and be less emotional about technical disagreements.Adjective
The right way to put it the way I put it - you are not guranteed to have a specific number of temporary objects created at any given moment. Any dependence on the number is an indication of bad-formed code. You need not even mention side effects. As to my tone I find it without flaw.Asuncionasunder
@Asuncionasunder The copy constructor for shared_ptr in the GNU ISO C++ library is defined as shared_ptr(const shared_ptr&) noexcept = default;. I feel confident in saying this is side-effect free.Husbandman
@CaptainGiraffe what is your actual definition of a side-effect?Asuncionasunder
@Asuncionasunder Not altering any state besides the actual construction. If you consider shared_ptr's CCTOR to have side-effects you cannot write any constructor without side-effects and the term becomes moot.Husbandman
@CaptainGiraffe, what is 'any state' and what is 'actual construction'? If during the construction of obj A, I go to some object B and change something here, is it a side effect?Asuncionasunder
@Asuncionasunder Of course objB->nonConst() is a side-effect. You stated that the copy constructor in question has side-effects. What is that side-effect?Husbandman
@CaptainGiraffe, gotcha. It means, copy-constructor for aliased shared_ptr has side effects. Is it bad?Asuncionasunder
P
1

Elision is the merging of object lifetimes and identities.

Elision can occur between a temporary (anonymous object) and the named object it is used to (directly) construct, and between a function-local variable that is not a function argument and the return value of a function.

Elision commutes, in effect. (If object A and B are elided together, and B and C are elided together, then in effect A and C are elided together).

To elide the return value of a function with a variable outside the function, you must directly construct that return value from the return value. While in some contexts the constructed variable can be named prior to being constructed, using it (in a way similar to the above code) is undefined behavior prior to the constructor occurring.

This constructor of the external variable is sequenced after the body of func, and hence after func is called. So it could not have occurred prior to func being called.

Here is an example of a case where we named a variable prior to it being constructed, and pass it to func, then initialize the variable with the return value of func. It appears that the compiler chose not to elide in this case, but as observed below in the comments, id actually did: my invocation of UB hid the elision. (the convolution was an attempt to prevent the compiler from pre-computing the values of test.x and test.y).

Painty answered 23/10, 2015 at 19:20 Comment(2)
Interesting. In your example func does get identical addresses for in and return value on initialization of test, but its code carefully reads in before touching the corresponding part of out, thus behaving as if in and out were different variables.Brambling
@ruslan classic UB: it behaves correctly for a correct programm, which my code is not.Painty

© 2022 - 2024 — McMap. All rights reserved.