Home > Back-end >  Why does the compiler create a variable that's only used once?
Why does the compiler create a variable that's only used once?

Time:07-11

Consider the following two examples, both compiled with g (GCC) 12.1.0 targetting x86_64-pc-linux-gnu: g -O3 -std=gnu 20 main.cpp.

Example 1:

#include <iostream>

class Foo {
public:
    Foo(){ std::cout << "Foo constructor" << std::endl;}
    Foo(Foo&& f) { std::cout << "Foo move constructor" << std::endl;}
    Foo(const Foo& f) {std::cout << "Foo copy constructor" << std::endl;}

    ~Foo() { std::cout << "Foo destructor" << std::endl;}
};

class Baz {
private:
    Foo foo;
public:
    Baz(Foo foo) : foo(std::move(foo)) {}
    ~Baz() { std::cout << "Baz destructor" << std::endl;}
};

Foo bar() {
    return Foo();
}

int main() {
    Baz baz(bar());
    return 0;
}

Output:

Foo constructor
Foo move constructor
Foo destructor
Baz destructor
Foo destructor

Example 2:

#include <iostream>

class Foo {
public:
    Foo(){ std::cout << "Foo constructor" << std::endl;}
    Foo(Foo&& f) { std::cout << "Foo move constructor" << std::endl;}
    Foo(const Foo& f) {std::cout << "Foo copy constructor" << std::endl;}

    ~Foo() { std::cout << "Foo destructor" << std::endl;}
};

class Baz {
private:
    Foo foo;
public:
    Baz(Foo foo) : foo(std::move(foo)) {}
    ~Baz() { std::cout << "Baz destructor" << std::endl;}
};

Foo bar() {
    return Foo();
}

int main() {
    // DIFFERENCE IS RIGHT HERE
    Foo f = bar();
    Baz baz(f);
    return 0;
}

Output:

Foo constructor
Foo copy constructor
Foo move constructor
Foo destructor
Baz destructor
Foo destructor
Foo destructor

So the difference here is that there is an additional instance of Foo, which needs to be handled. I find this difference quite shocking, as my (apparently untrained) eyes would not even consider these pieces of code effectively different. Moreover, a collegue might ask me to turn example 1 into example 2 for readability and suddenly I will be incurring a performance penalty!

So why are example 1 and 2 so different according to the compiler/C spec that the resulting code suddenly needs to copy? Why can the compiler not generate the same code for both cases?

CodePudding user response:

why are example 1 and 2 so different according to the compiler/C spec that the resulting code suddenly needs to copy?

Because in example 2 you have a variable named f of type Foo which is used to initialize baz while in example 1 you're initializing baz with a prvalue of type Foo which will result in directly constructing the parameter foo and then moving from it.

To be more precise, in example 2 since f in the statement Baz baz(f); is an lvalue expression and so the copy ctor Foo::Foo(const Foo&) will be used to initialize the parameter Foo foo of the converting ctor Baz::Baz(Foo). Thus we get the output corresponding to the copy ctor. Next due to foo(std::move(foo)) we will also get a call to the move ctor.

While in example 1, bar() is a prvalue of type Foo. This means that the parameter Foo foo of Baz::Baz(Foo) will be directly constructed(due to mandatory copy elison in C 17) and then due to foo(std::move(foo)) we also get a call to the move ctor.


Let's see step by step explanation.

Case 1

Here we consider example:

Baz baz(bar());

Step 1: The parameterized ctor Baz::Baz(Foo foo) will be used here.

Step 2: Since bar() is a prvalue, the parameter named foo of the parameterized ctor Baz::Baz(Foo foo) will be directly constructed due to mandatory copy elison in C 17. Thus we get the first statement of the output Foo constructor.

Step 3 Now due to foo(std::move(foo)) we get the call to the move ctor Foo::Foo(Foo&& f) and so the second statement of the output Foo move constructor.


Case 2

Here we consider:

Foo f = bar();  //this will call the default ctor and so we get the first statement of the output
//------v------->f is an lvalue
Baz baz(f);

Step 1 Here also the parameterized ctor Baz::Baz(Foo foo) will be used.

Step 2 But this time since f is an lvalue expression the copy ctor Foo::Foo(const Foo&) will be used to initialized the parameter named foo of the parameterized ctor Baz::Baz(Foo foo). Thus we get the second statement of the output Foo copy constructor.

Step 3 Next due to foo(std::move(foo)) we get a call to the move ctor and the third statement of the output Foo move constructor

CodePudding user response:

In

Foo f = bar();
Baz baz(f);
return 0

It is indeed obvious that f will never be used again and the compiler probably even works this out. However the compiler isn't allowed to move f into baz, all optimisations done by the compiler must result in your program having the same behaviour (if the program is well defined to start with). Especially in your case where the move and copy constructors print different things, making this optimisation would change the behaviour of your program. In a more realistic program the compiler may be able to deduce that the code would have the same behaviour with a move or a copy and change a copy into a move but it's always best to give the compiler as much help as possible and not rely on it making optimisations.

If you know that you don't need f any more then you can tell the compiler this by converting it to an rvalue reference using std::move, then it will use the move constructor (if available):

Foo f = bar();
Baz baz(std::move(f));
return 0
  • Related