3

Minimal example:

#include <iostream>

struct my_class
{
    int i;
    my_class() : i(0) { std::cout << "default" << std::endl; }
    my_class(const my_class&) { std::cout << "copy" << std::endl; }
    my_class(my_class&& other) { std::cout << "move" << std::endl; }
    my_class(const my_class&& other) { std::cout << "move" << std::endl; }
};

my_class get(int c)
{
    my_class m1;
    my_class m2;
    return (c == 1) ? m1 : m2; // A
    //return (c == 1) ? std::move(m1) : m2; // B
    //return (c == 1) ? m1 : std::move(m2); // C
}

int main()
{
    bool c;
    std::cin >> c;
    my_class m = get(c);
    std::cout << m.i << std::endl; // nvm about undefinedness
    return 0;
}

Compiled:

g++ -std=c++11 -Wall -O3 ctor.cpp -o ctor # g++ v 4.7.1

Input:

1

Output:

default
default
copy
-1220217339

This is the In/Output with line A or line C. If I use line B, instead, I get std::move for some strange reason. In all versions, the output does not depend on my input (except for the value of i).

My questions:

  • Why do versions B and C differ?
  • Why, at all, does the compiler make a copy in cases A and C?
Johannes
  • 2,901
  • 5
  • 30
  • 50
  • http://stackoverflow.com/questions/18486598/c-unexpected-behaviour-where-are-my-temporaries – Alec Teal Nov 16 '13 at 14:54
  • @AlecTeal Are you sure? I don't expect copy elision, and my question is not related to eliding constructors/RVO (the compiler can not do RVO here). – Johannes Nov 16 '13 at 14:58
  • No but it teaches about how construction and stuff happens, if you have "T t; t=otherT;" it will use assignment, if you say "T t = otherT;" even thogh you wrote "=" it wont default construct and assign. If it's an r-value it will move. – Alec Teal Nov 16 '13 at 15:01
  • I don't quite see why you're confused, how is this behavior unexpected? – Alec Teal Nov 16 '13 at 15:03
  • @Alec Teal he is expcecting construction by move constructor, as the value returned by `get` should be expiring, iirc – Erbureth Nov 16 '13 at 15:07
  • 1
    BTW, just tested your code against Clang 3.4 and G++ 4.8.2, same result – Erbureth Nov 16 '13 at 15:08
  • @AlecTeal It makes no difference whether I use `T t = otherT;` or `T t = otherT;`. Moreover, my question comes without an `operator=` (even without a default one since it is being deleted). – Johannes Nov 16 '13 at 15:14

4 Answers4

4

Where is the surprise...? You are returning local objects but you are not directly returning them. If you'd return a local variable directly, you'll get move construction:

my_class f() {
    my_class variable;
    return variable;
}

The relevant clause is, I think, 12.8 [class.copy] paragraph 32:

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. [...]

However, choosing a named object to be selected from a conditional operator isn't eligible for copy elision: the compiler can't know until after the objects are constructed which of the objects to return and copy elision is based on constructing the object readily in the location where it needs to go.

When you have a condition operator, there are two fundamental situations:

  1. Both branches produce exactly the same type and the result will be a reference to the result.
  2. The branches differ somehow and the result will be a temporary constructed from the selected branch.

That is, when returning c == 1? m1: m2 you get a my_class& which is an lvalue and it is, thus, copied to produce the return value. You probably want to use std::move(c == 1? m1: m2) to move the selected local variable.

When you use c == 1? std::move(m1): m2 or c == 1? m1: std::move(m2) the types differ and you get the result of

return c == 1? my_class(std::move(m1)): my_class(m2);

or

return c == 1? my_class(m1): my_class(std::move(m2));

That is, depending on how the expression is formulated the temporary is copy constructed in one branch and move constructed on the other branch. Which branch is chosen depends entirely on the value of c. In both cases the result of the conditional expression is eligible for copy elision and the copy/move used to construct the actual result is likely to be elided.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Thanks, this explains the second question. But for the first, any idea why versions B and C differ? – Johannes Nov 16 '13 at 15:44
  • @Johannes: actually, it isn't quite accurate but I'll update in a bit. The reason `B` and `C` differ is how the temporary return is created: it is moving one but not the other (but that isn't quite as clear as it should be in current answer). – Dietmar Kühl Nov 16 '13 at 15:52
2

Conditional operator effect!

You're returning through a conditional operator

return (c == 1) ? m1 : m2;

The second and third operands have the same type; the result is of that type. If the operands have class type, the result is a prvalue temporary of the result type, which is copy-initialized from either the second operand or the third operand depending on the value of the first operand. [§ 5.16/6]

Then you have a copy. This code has the your expected result.

if (c==1)
   return m1;
else
   return m2;
masoud
  • 55,379
  • 16
  • 141
  • 208
  • You say it is "depending on the result type"? But the result type is the same in B and C. (Tried it with a decltype, I get "8my_class" in both versions). – Johannes Nov 16 '13 at 16:23
  • @Johannes: No, as the quote says if the types are the same, then the result will be **copy-initialized**. – masoud Nov 16 '13 at 16:24
  • How does this explain that the `std::move` on the second operand has a different effect than that on the third one? – Johannes Nov 16 '13 at 16:32
1
  1. If my_class is as expensive to copy as copying an int, the compiler isn't motivated to eliminate the copies, in fact, it is motivated to do copies. Don't forget that your get(int c) function can be completely inlined! It can lead to a very confusing output. You need to motivate the compiler to do its best to eliminate the copies by adding a big, heavy payload to your class that is expensive to copy.

  2. Furthermore, instead of relying on undefined behavior, try to write code that tells you in a well-defined manner whether a move or a copy occurred or not.

  3. There are 2 more interesting cases: (i) when you apply move on both arguments of the ternary conditional operator and (ii) when you return through if-else instead of the conditional operator.


I rearranged your code: I gave my_class a heavy payload that is really expensive to copy; I added a member function that tells you in a well-defined manner if the class has been copied or not; I added the 2 other interesting cases.

#include <iostream>
#include <string>
#include <vector>

class weight {
public:  
    weight() : v(1024, 0) { };
    weight(const weight& ) : v(1024, 1) { }
    weight(weight&& other) { v.swap(other.v); }
    weight& operator=(const weight& ) = delete;
    weight& operator=(weight&& ) = delete;
    bool has_been_copied() const { return v.at(0); }
private:
    std::vector<int> v;
};

struct my_class {
    weight w;
};

my_class A(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? m1 : m2;
}

my_class B(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? std::move(m1) : m2;
}

my_class C(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? m1 : std::move(m2);
}

my_class D(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? std::move(m1) : std::move(m2);
}

my_class E(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    if (c==1) 
      return m1;
    else
      return m2;
}

int main(int argc, char* argv[]) {

    if (argc==1) {
      return 1; 
    }

    int i = std::stoi(argv[1]);

    my_class a = A(i);
    std::cout << a.w.has_been_copied() << std::endl;

    my_class b = B(i);
    std::cout << b.w.has_been_copied() << std::endl;

    my_class c = C(i);
    std::cout << c.w.has_been_copied() << std::endl;

    my_class d = D(i);
    std::cout << d.w.has_been_copied() << std::endl;

    my_class e = E(i);
    std::cout << e.w.has_been_copied() << std::endl;
}

Output with ./a.out 0

my_class A(int)
1
my_class B(int)
1
my_class C(int)
0
my_class D(int)
0
my_class E(int)
0

Output with ./a.out 1

my_class A(int)
1
my_class B(int)
0
my_class C(int)
1
my_class D(int)
0
my_class E(int)
0

As to what happens and why, others have already answered it as I was writing up this answer. If you go through the conditional operator, you lose the eligibility to copy elision. You can still get away with a move construction if you apply move. If you look at the output, that's exactly what happens. I have tested it with both clang 3.4 trunk and gcc 4.7.2 at optimization level -O3; the same output is obtained.

Community
  • 1
  • 1
Ali
  • 56,466
  • 29
  • 168
  • 265
-1

the compiler doesn't HAVE to move, the point of move is to be a lot faster than a copy and destruct. BUT the two yield the same result.

Alec Teal
  • 5,770
  • 3
  • 23
  • 50
  • I think a copy would also involve a destruction. Are you confusing copying with RVO? – Johannes Nov 16 '13 at 15:36
  • The compiler doesn't have a choice whether to move-copy or copy. It can only chose to elide a copy or not. – juanchopanza Nov 16 '13 at 15:47
  • @juanchopanza I believe it does, I believe that is the point of move, to help us avoid the penalty of copying large structures. Can you cite the standard? I know it published a tentative list as to what consitutes an r-value, as the idea is explored – Alec Teal Nov 16 '13 at 16:17
  • The point is that, unlike copy elision, whether to move copy construct or copy construct is completely specified by the standard. – juanchopanza Nov 16 '13 at 17:00
  • @juanchopanza I don't believe you are right, I know the specification defines properties such that if they are met it is an R-value (it is not an IFF relationship) I don't think it is standard where they are used. Please correct me if I am wrong! – Alec Teal Nov 16 '13 at 17:19