简体   繁体   中英

Why is there a copy when creating an rvalue and passing it to a function?

To better understand copy elision I wrote a test app that did a simple action in copy and move constructors/assignment operators and counted the times it was copied or moved. I noticed however that there was a copy when I created an rvalue and passed it directly rather than creating a lvalue then passing it in.

I'm trying to understand if this is compiler defined or part of the language specification? Additionally I'm trying to understand why it wouldn't be omitted as it seems like it should just be a construction rather than a copy?

godbolt of what I tested

struct Bar {
    Bar() {}

    Bar(const Bar& a)
    : cp_count{a.cp_count + 1}, mv_count{a.mv_count} {}
    Bar& operator=(const Bar& a) {
        cp_count = a.cp_count + 1;
        mv_count = a.mv_count;
        return *this;
    }

    Bar(Bar&& a)
     : cp_count{a.cp_count}, mv_count{a.mv_count + 1} {}
    Bar& operator=(Bar&& a) {
        cp_count = a.cp_count;
        mv_count = a.mv_count + 1;
        return *this;
    }

    int cp_count = 0;
    int mv_count = 0;
};

struct Foo {
    Bar bar;
    std::function<void(Bar)> setter;
    std::function<void(Bar)> setter2;
    
    Foo() {
        setter = [this](Bar a) {
            bar = a;
        };

        setter2 = [this](Bar a) {
            bar = std::move(a);
        };
    }
};

int main() {
    std::cout << std::endl << "base line" << std::endl;
    {
        Foo foo;
        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }

    std::cout << std::endl << "in-place then copy" << std::endl;
    {
        Foo foo;
        foo.setter(Bar{});

        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }

    std::cout << std::endl << "copy then copy" << std::endl;
    {
        Foo foo;
        Bar bar{};
        foo.setter(bar);

        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }

    std::cout << std::endl << "move then copy" << std::endl;
    {
        Foo foo;
        Bar bar{};
        foo.setter(std::move(bar));

        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }

    std::cout << std::endl << "in-place then move" << std::endl;
    {
        Foo foo;
        foo.setter2(Bar{});

        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }

    std::cout << std::endl << "copy then move" << std::endl;
    {
        Foo foo;
        Bar bar{};
        foo.setter2(bar);

        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }

    std::cout << std::endl << "move then move" << std::endl;
    {
        Foo foo;
        Bar bar{};
        foo.setter2(std::move(bar));

        std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
    }
}

The output I received from my testing

base line
mv_count = 0 cp_count = 0

in-place then copy
mv_count = 1 cp_count = 1

copy then copy
mv_count = 1 cp_count = 2

move then copy
mv_count = 2 cp_count = 1

in-place then move
mv_count = 2 cp_count = 0

copy then move
mv_count = 2 cp_count = 1

move then move
mv_count = 3 cp_count = 0

The additional moves seen in the output are caused by std::function<void(Bar)> . Change the definition of Foo to

struct Foo 
{
    Bar bar;
    void setter(Bar a) { bar = a; }
    void setter2(Bar a) { bar = std::move(a); }
};

and the output becomes

base line
mv_count = 0 cp_count = 0

in-place then copy
mv_count = 0 cp_count = 1

copy then copy
mv_count = 0 cp_count = 2

move then copy
mv_count = 1 cp_count = 1

in-place then move
mv_count = 1 cp_count = 0

copy then move
mv_count = 1 cp_count = 1

move then move
mv_count = 2 cp_count = 0

which is what you should expect. For example, for in-place then copy , the parameter a of setter is initialized from the argument prvalue Bar{} (guaranteed copy elision in C++17), then is copy-assigned to bar (one invocation of Bar 's copy-assignment operator); overall, no moves, one copy.


When setter is std::function<void(Bar)> , setter(x) calls std::function 's operator()(Bar arg) , which wraps your lambda closure object's operator()(Bar a) . It basically passes std::forward<Bar>(arg) to your operator() , adding one move construction (of a from std::forward<Bar>(arg) ) to all cases, which explains the results you were seeing.

The additional move construction cannot be elided, since

  • std::forward<Bar>(arg) is an xvalue, not a prvalue (no guaranteed copy elision);
  • Bar 's move constructor has side effects;
  • we're not in any of the cases specified by [class.copy.elision]/1 (not a return , not a throw , not an exception-declaration ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM