传递右值与左值向量

passing a rvalue vs lvalue vector

本文关键字:向量      更新时间:2023-10-16

我有一个方法my func,它从get_vec获取一个向量并将其传递给类A的某个构造函数。

class A
{
public:
A(std::vector<int>&& vec) : m_vec(std::move(vec)) {}
std::vector<int> m_vec;
};
std::vector<int> get_vec()
{
std::vector<int> res;
// do something
return res;
}
void my_func()
{
std::vector<int> vec = get_vec();
A(std::move(vec));
}

https://godbolt.org/z/toQ5KZ

理想情况下,我希望向量被构造一次,但在这个例子中,我在get_vec中创建向量,然后将其复制到my_func,将其移动到构造函数,然后再次将其移动到A::m_vec

传递向量的正确和有效的方法是什么?

例如,如果您执行A a(get_vec());std::vector<int>&& m_vec成员将导致悬空引用。

正确且安全的方法是按值赋予该成员:

std::vector<int> m_vec;

编译器优化通常仅在不更改可观察行为时才允许。然而,复制省略是少数几种允许以性能的名义更改程序输出(与抽象机器的输出相比)的情况之一。

我们可以用一个模拟向量来证明这一点,它的特殊成员函数有副作用(例如写入volatile变量):

class MyVec
{
public:
MyVec() { x = 11; };
MyVec(const MyVec&) { x = 22; }
MyVec(MyVec&&) { x = 33; }
MyVec& operator=(const MyVec&) { x = 44; return *this; }
MyVec& operator=(MyVec&&) { x = 55; return *this; }
// Make this the same size as a std::vector
void* a = nullptr;
void* b = nullptr;
void* c = nullptr;
};

如果我们检查优化的程序集,我们可以看到只有一个默认构造函数和一个移动构造函数的副作用实际上保留在my_func中,其他一切都被优化掉了。第一个默认构造函数是内联get_vec,另一个是A构造函数中的移动。这与您从临时构造成员时一样高效。

这是允许的,因为在从函数return时,以及从(或多或少)临时初始化时,可以省略副本。后者曾经是"您可以在X x = getX();中省略副本",但自 17 C++版本以来,从未创建过临时版本(https://en.cppreference.com/w/cpp/language/copy_initialization)。

我决定重写这个答案,因为豪尔赫·佩雷斯(Jorge Perez)善意地指出原始答案是有缺陷的,并且因为其他答案都没有真正完整地解决原始问题。

我首先编写了一个简单的测试程序:

#include <iostream>
class Moveable
{
public:
Moveable ()  { std::cout << "Constructorn"; }
~Moveable ()  { std::cout << "Destructorn"; }
Moveable (const Moveable&) { std::cout << "Copy constructorn"; }
Moveable& operator= (const Moveable&) { std::cout << "Copy assignmentn"; return *this; }
Moveable (const Moveable&&) { std::cout << "Move constructorn"; }
Moveable& operator= (const Moveable&&) { std::cout << "Move assignmentn"; return *this; }
};
class A
{
public:
A (Moveable &&m) : m_m (std::move (m)) {}
Moveable m_m;
};
Moveable get_m ()
{
Moveable res;
return res;
}
int main ()
{
Moveable m = get_m ();
A (std::move (m));
}

它给出了以下输出:

Constructor
Move constructor
Destructor
Destructor

因此,您可以立即看到代码中的低效率并不像您想象的那么糟糕 - 只有一个动作,没有副本。

现在,正如其他人所说:

Moveable m = get_m ();

不会因为指定返回值优化 (NRVO) 而复制任何内容。

而且只有一个动作,因为:

A (std::move (m));

实际上没有移动任何东西(它只是一个演员)。

现场演示

关于此举,可以说,如果你改变这一点,正在发生的事情会更清楚:

A (Moveable&& m) : m_m (std::move (m)) {}

对此:

A (Moveable& m) : m_m (std::move (m)) {}

因为您可以更改此设置:

A (std::move (m));

对此:

A {m};

并且仍然获得相同的输出(您需要大括号以避免"最烦人的解析")。

现场演示