Class with no destructor returns copy of object but when I add a destructor it returns the same object

I was playing around with classes in C++, specifically implementing a private constructor with a static "create" function that returns the object.

// main.cpp
#include <iostream>

class Foo {
public:
    static Foo create_foo() {    
        Foo foo{};
        std::cout << "Foo::create_foo():: Address of foo: " << &foo << std::endl;

        return foo;
    }
private:
    Foo() { std::cout << "Foo::Foo():: Constructor" << std::endl; }
};

int main() {
    auto foo = Foo::create_foo();
    std::cout << "main():: Address of foo: " << &foo << std::endl;
    
    return 0;
}

Running the code above creates two different Foo objects: one in Foo::create_foo() and one in main() as seen from the output:

Foo::Foo():: Constructor
Foo::create_foo():: Address of foo: 0x7ffe5d5e60bf
main():: Address of foo: 0x7ffe5d5e60ef

My first inclination was "okay, it's calling the copy constructor." So I added the following public copy constructor:

Foo(Foo const&) { std::cout << "Foo::Foo(Foo const&):: Copy constructor" << std::endl; }

This is where my confusion lies, by simply adding a copy constructor, Foo::create_foo() and main() now contain the same object:

Foo::Foo():: Constructor
Foo::create_foo():: Address of foo: 0x7ffe53c9d63f
main():: Address of foo: 0x7ffe53c9d63f

This same behavior happens as well if I add a public destructor:

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

Output:

Foo::Foo():: Constructor
Foo::create_foo():: Address of foo: 0x7ffed84a400f
main():: Address of foo: 0x7ffed84a400f
Foo::~Foo():: Destructor

I am just curious as to why adding an additional (copy) constructor or destructor changes the behavior of my static function. Could it be some compiler optimization? I am using gcc version 10.2.1 20201125 (Red Hat 10.2.1-9) (GCC) and compiling without any flags (g++ -o main main.cpp).


All the potential copies/moves of Foo in the code are allowed to be elided by the compiler. Since C++17 this is even mandatory for all but the move construction of the return value of create_foo from foo.

This latter case is called named return value optimization and is still an optional optimization that the compiler may choose to apply and elide the constructor call.

You are not compiling with optimizations enabled. Add the -O2 flag and the NRVO is applied in all cases you describe.

I don't know why GCC does apply NRVO even without optimization flags only if you add the special member functions, but it may be equally surprising that it applies the NRVO at all without optimization flags.

With NRVO applied (and all other elisions also applied, whether mandatory or not), there will be only a single Foo object, directly constructed by create_foo into foo in main.