为什么在创建右值并将其传递给函数时会有一个副本?

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

为了更好地理解复制省略,我编写了一个测试应用程序,它在复制和移动 constructors/assignment 运算符中执行了一个简单的操作,并计算了它被复制或移动的次数。但是我注意到当我创建一个右值并直接传递它而不是创建一个左值然后传递它时有一个副本。

我想知道这是编译器定义的还是语言规范的一部分?另外我试图理解为什么它不会被省略,因为它看起来应该只是一个构造而不是一个副本?

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;
    }
}

我从测试中收到的输出

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

输出中看到的额外移动是由 std::function<void(Bar)> 引起的。将 Foo 的定义更改为

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

输出变为

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

这是您应该期待的。例如,对于 in-place then copysetter 的参数 a 是从参数 prvalue Bar{} 初始化的(C 中保证复制省略++17),然后被复制赋值给 barBar 的复制赋值运算符的一次调用);整体,无招,一份。


setterstd::function<void(Bar)> 时,setter(x) 调用 std::functionoperator()(Bar arg),它包装了您的 lambda 闭包对象的 operator()(Bar a)。它基本上 passes std::forward<Bar>(arg) to your operator(),在所有情况下添加一个移动结构(来自 std::forward<Bar>(arg)a),这解释了您看到的结果。

不能省略额外的移动构造,因为

  • std::forward<Bar>(arg) 是一个 xvalue,不是 prvalue(不保证复制省略);
  • Bar的移动构造函数有副作用;
  • 我们不属于 [class.copy.elision]/1 指定的任何情况(不是 return,不是 throw,不是 异常声明).