Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler's choice between move and copy constructor

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?
like image 364
Johannes Avatar asked Jun 10 '26 14:06

Johannes


1 Answers

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.

like image 107
Dietmar Kühl Avatar answered Jun 14 '26 10:06

Dietmar Kühl