传递右值与左值向量
passing a rvalue vs lvalue vector
我有一个方法 my func
从 get_vec
获取向量并将其传递给 class 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));
}
理想情况下,我希望向量被构建一次,但在这个例子中,我在 get_vec
中创建向量,然后将其复制到 my_func
,将其移动到构造函数,然后再次移动它到 A::m_vec
.
传递向量的正确有效方法是什么?
例如,std::vector<int>&& m_vec
成员如果您这样做 A a(get_vec());
,将导致悬空引用。
正确且安全的方法是按值获取该成员:
std::vector<int> m_vec;
编译器优化通常只有在不改变可观察行为的情况下才被允许。但是,copy elision 是为数不多的以性能名义甚至允许更改程序输出(与抽象机的输出相比)的情况之一。
我们可以用一个模拟向量来证明这一点,它的特殊成员函数有副作用(例如写入 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;
};
如果我们检查 optimized assembly,我们可以看到实际上只有一个默认构造函数和一个移动构造函数的副作用保留在 my_func
中,其他所有内容都被优化掉了。第一个默认构造函数是内联的get_vec
,另一个是A
的构造函数中的move。从临时构造成员时,这与您可能的效率一样高。
这是允许的,因为当从函数 return
ing 以及从(或多或少)临时文件初始化时,可以省略副本。后者曾经是 "you can elide the copy in X x = getX();
",但自从 C++ 17 版本以来,从未创建任何临时文件 (https://en.cppreference.com/w/cpp/language/copy_initialization)。
我决定重写这个答案,因为 Jorge Perez 友善地指出原来的问题是有缺陷的,而且 none 的其他答案确实完整地解决了原来的问题。
我先写了一个简单的测试程序:
#include <iostream>
class Moveable
{
public:
Moveable () { std::cout << "Constructor\n"; }
~Moveable () { std::cout << "Destructor\n"; }
Moveable (const Moveable&) { std::cout << "Copy constructor\n"; }
Moveable& operator= (const Moveable&) { std::cout << "Copy assignment\n"; return *this; }
Moveable (const Moveable&&) { std::cout << "Move constructor\n"; }
Moveable& operator= (const Moveable&&) { std::cout << "Move assignment\n"; 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 ();
不复制任何东西,因为 Named Return Value Optimisation (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};
并且仍然得到相同的输出(你需要大括号来避免 'the most vexing parse')。
我有一个方法 my func
从 get_vec
获取向量并将其传递给 class 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));
}
理想情况下,我希望向量被构建一次,但在这个例子中,我在 get_vec
中创建向量,然后将其复制到 my_func
,将其移动到构造函数,然后再次移动它到 A::m_vec
.
传递向量的正确有效方法是什么?
std::vector<int>&& m_vec
成员如果您这样做 A a(get_vec());
,将导致悬空引用。
正确且安全的方法是按值获取该成员:
std::vector<int> m_vec;
编译器优化通常只有在不改变可观察行为的情况下才被允许。但是,copy elision 是为数不多的以性能名义甚至允许更改程序输出(与抽象机的输出相比)的情况之一。
我们可以用一个模拟向量来证明这一点,它的特殊成员函数有副作用(例如写入 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;
};
如果我们检查 optimized assembly,我们可以看到实际上只有一个默认构造函数和一个移动构造函数的副作用保留在 my_func
中,其他所有内容都被优化掉了。第一个默认构造函数是内联的get_vec
,另一个是A
的构造函数中的move。从临时构造成员时,这与您可能的效率一样高。
这是允许的,因为当从函数 return
ing 以及从(或多或少)临时文件初始化时,可以省略副本。后者曾经是 "you can elide the copy in X x = getX();
",但自从 C++ 17 版本以来,从未创建任何临时文件 (https://en.cppreference.com/w/cpp/language/copy_initialization)。
我决定重写这个答案,因为 Jorge Perez 友善地指出原来的问题是有缺陷的,而且 none 的其他答案确实完整地解决了原来的问题。
我先写了一个简单的测试程序:
#include <iostream>
class Moveable
{
public:
Moveable () { std::cout << "Constructor\n"; }
~Moveable () { std::cout << "Destructor\n"; }
Moveable (const Moveable&) { std::cout << "Copy constructor\n"; }
Moveable& operator= (const Moveable&) { std::cout << "Copy assignment\n"; return *this; }
Moveable (const Moveable&&) { std::cout << "Move constructor\n"; }
Moveable& operator= (const Moveable&&) { std::cout << "Move assignment\n"; 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 ();
不复制任何东西,因为 Named Return Value Optimisation (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};
并且仍然得到相同的输出(你需要大括号来避免 'the most vexing parse')。