如果 const 引用也只需要一个副本,为什么在 C++11 中推荐按值传递(如果需要副本)?
Why is passing by value (if a copy is needed) recommended in C++11 if a const reference only costs a single copy as well?
我试图理解移动语义、右值引用、std::move
等。我一直在尝试通过搜索本网站上的各种问题来弄清楚为什么传递 const std::string &name
+如果需要副本,_name(name)
不如 std::string name
+ _name(std::move(name))
推荐。
如果我没理解错的话,下面需要一个副本(通过构造函数)加上一个移动(从临时到成员):
Dog::Dog(std::string name) : _name(std::move(name)) {}
另一种(也是老式的)方法是通过引用传递它并复制它(从对成员的引用):
Dog::Dog(const std::string &name) : _name(name) {}
如果第一种方法需要复制和移动两者,而第二种方法只需要一个副本,那么第一种方法如何成为首选,并且在某些情况下更快?
当消费数据时,您需要一个可以消费的对象。当您获得 std::string const&
时,您 将 必须独立于是否需要参数来复制对象。
当按值传递对象时,如果必须复制对象,则对象将被复制,即当传递的对象不是临时对象时。但是,如果它恰好是临时对象,则可以就地构建对象,即,任何副本都可能已被删除,您只需支付移动构建费用。也就是说,有可能实际上没有复制发生。
考虑用左值和右值调用各种选项:
Dog::Dog(const std::string &name) : _name(name) {}
无论是用左值还是右值调用,这都需要一个副本,以从 name
初始化 _name
。移动不是一种选择,因为 name
是 const
。
Dog::Dog(std::string &&name) : _name(std::move(name)) {}
这只能用右值调用,它会移动。
Dog::Dog(std::string name) : _name(std::move(name)) {}
当使用左值调用时,这将复制以传递参数,然后移动以填充数据成员。当使用右值调用时,这将移动到传递参数,然后移动到填充数据成员。在右值的情况下,可以省略传递参数的移动。因此,用左值调用它会导致一次复制和一次移动,而用右值调用它会导致一到两次移动。
最佳解决方案是同时定义(1)
和(2)
。解决方案 (3)
可以相对于最优解有额外的移动。但是编写一个函数比编写两个几乎相同的函数更短且更易于维护,而且移动被认为是便宜的。
当使用可隐式转换为字符串的值调用时,如 const char*
,会发生隐式转换,这涉及长度计算和字符串数据的副本。然后我们陷入了右值的情况。在这种情况下,使用 string_view
提供了另一种选择:
Dog::Dog(std::string_view name) : _name(name) {}
当使用字符串左值或右值调用时,会生成一个副本。当使用 const char*
调用时,进行一次长度计算并复制一份。
首先简短回答:通过 const& 调用将始终花费一个副本。根据条件按值调用可能只花费一步 .但这取决于(请查看下面的代码示例以了解此 table 所指的场景):
lvalue rvalue unused lvalue unused rvalue
------------------------------------------------------
const& copy copy - -
rvalue&& - move - -
value copy, move move copy -
T&& copy move - -
overload copy move - -
所以我的执行摘要是如果
- 一步很便宜,因为可能会有额外的一步
- 无条件使用该参数。如果不使用参数,按值调用也会花费一个副本,例如因为 if 子句或某事。
按价值调用
考虑一个用于复制其参数的函数
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
private:
std::vector<std::string> names;
};
如果左值传递给 name_it
,在右值的情况下,您也将有两个复制操作。那很糟糕,因为我可以移动右值。
一种可能的解决方案 是为右值编写重载:
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
void name_it(std::string&& newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
这解决了问题,一切都很好,尽管您有两个代码,两个函数的代码完全相同。
另一个可行的解决方案 是使用完美转发,但这也有几个缺点,(例如完美转发函数非常贪婪并且使现有的重载 const& 函数无用,通常他们需要在头文件中,他们在目标代码中创建几个函数等等。)
class Dog {
public:
template<typename T>
void name_it(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<std::string> names;
};
另一种解决方案 是使用按值调用:
class Dog {
public:
void name_it(std::string newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
重要的是,正如你提到的std::move
。这样您将拥有一个用于右值和左值的函数。您将移动右值但接受左值的额外移动,这可能很好如果移动便宜并且您复制或移动参数而不考虑条件。
所以最后我真的认为推荐一种方式而不是其他方式是完全错误的。这在很大程度上取决于。
#include <vector>
#include <iostream>
#include <utility>
using std::cout;
class foo{
public:
//constructor
foo() {}
foo(const foo&) { cout << "\tcopy\n" ; }
foo(foo&&) { cout << "\tmove\n" ; }
};
class VDog {
public:
VDog(foo name) : _name(std::move(name)) {}
private:
foo _name;
};
class RRDog {
public:
RRDog(foo&& name) : _name(std::move(name)) {}
private:
foo _name;
};
class CRDog {
public:
CRDog(const foo& name) : _name(name) {}
private:
foo _name;
};
class PFDog {
public:
template <typename T>
PFDog(T&& name) : _name(std::forward<T>(name)) {}
private:
foo _name;
};
//
volatile int s=0;
class Dog {
public:
void name_it_cr(const foo& in_name) { names.push_back(in_name); }
void name_it_rr(foo&& in_name) { names.push_back(std::move(in_name));}
void name_it_v(foo in_name) { names.push_back(std::move(in_name)); }
template<typename T>
void name_it_ur(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<foo> names;
};
int main()
{
std::cout << "--- const& ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_cr(my_foo);
std::cout << "rvalue:";
b.name_it_cr(foo());
}
std::cout << "--- rvalue&& ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue: -\n";
std::cout << "rvalue:";
a.name_it_rr(foo());
}
std::cout << "--- value ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_v(my_foo);
std::cout << "rvalue:";
b.name_it_v(foo());
}
std::cout << "--- T&&--\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_ur(my_foo);
std::cout << "rvalue:";
b.name_it_ur(foo());
}
return 0;
}
输出:
--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move
除了性能原因之外,当副本在按值构造函数上抛出异常时,它首先抛给调用者,而不是在构造函数本身内。这使得编写 noexcept 构造函数变得更容易,而不必担心资源泄漏或构造函数上的 try/catch 块。
struct A {
std::string a;
A( ) = default;
~A( ) = default;
A( A && ) noexcept = default;
A &operator=( A && ) noexcept = default;
A( A const &other ) : a{other.a} {
throw 1;
}
A &operator=( A const &rhs ) {
if( this != &rhs ) {
a = rhs.a;
throw 1;
}
return *this;
}
};
struct B {
A a;
B( A value ) try : a { std::move( value ) }
{ std::cout << "B constructor\n"; }
catch( ... ) {
std::cerr << "Exception in B initializer\n";
}
};
struct C {
A a;
C( A const &value ) try : a { value }
{ std::cout << "C constructor\n"; }
catch( ... ) {
std::cerr << "Exception in C initializer\n";
}
};
int main( int, char ** ) {
try {
A a;
B b{a};
} catch(...) { std::cerr << "Exception outside B2\n"; }
try {
A a;
C c{a};
} catch(...) { std::cerr << "Exception outside C\n"; }
return EXIT_SUCCESS;
}
会输出
Exception outside B2
Exception in C initializer
Exception outside C
我做了一个实验:
#include <cstdio>
#include <utility>
struct Base {
Base() { id++; }
static int id;
};
int Base::id = 0;
struct Copyable : public Base {
Copyable() = default;
Copyable(const Copyable &c) { printf("Copyable [%d] is copied\n", id); }
};
struct Movable : public Base {
Movable() = default;
Movable(Movable &&m) { printf("Movable [%d] is moved\n", id); }
};
struct CopyableAndMovable : public Base {
CopyableAndMovable() = default;
CopyableAndMovable(const CopyableAndMovable &c) {
printf("CopyableAndMovable [%d] is copied\n", id);
}
CopyableAndMovable(CopyableAndMovable &&m) {
printf("CopyableAndMovable [%d] is moved\n", id);
}
};
struct TEST1 {
TEST1() = default;
TEST1(Copyable c) : q(std::move(c)) {}
TEST1(Movable c) : w(std::move(c)) {}
TEST1(CopyableAndMovable c) : e(std::move(c)) {}
Copyable q;
Movable w;
CopyableAndMovable e;
};
struct TEST2 {
TEST2() = default;
TEST2(Copyable const &c) : q(c) {}
// TEST2(Movable const &c) : w(c)) {}
TEST2(CopyableAndMovable const &c) : e(std::move(c)) {}
Copyable q;
Movable w;
CopyableAndMovable e;
};
int main() {
Copyable c1;
Movable c2;
CopyableAndMovable c3;
printf("1\n");
TEST1 z(c1);
printf("2\n");
TEST1 x(std::move(c2));
printf("3\n");
TEST1 y(c3);
printf("4\n");
TEST2 a(c1);
printf("5\n");
TEST2 s(c3);
printf("DONE\n");
return 0;
}
结果如下:
1
Copyable [4] is copied
Copyable [5] is copied
2
Movable [8] is moved
Movable [10] is moved
3
CopyableAndMovable [12] is copied
CopyableAndMovable [15] is moved
4
Copyable [16] is copied
5
CopyableAndMovable [21] is copied
DONE
结论:
template <typename T>
Dog::Dog(const T &name) : _name(name) {}
// if T is only copyable, then it will be copied once
// if T is only movable, it results in compilation error (conclusion: define separate move constructor)
// if T is both copyable and movable, it results in one copy
template <typename T>
Dog::Dog(T name) : _name(std::move(name)) {}
// if T is only copyable, then it results in 2 copies
// if T is only movable, and you called Dog(std::move(name)), it results in 2 moves
// if T is both copyable and movable, it results in one copy, then one move.
我试图理解移动语义、右值引用、std::move
等。我一直在尝试通过搜索本网站上的各种问题来弄清楚为什么传递 const std::string &name
+如果需要副本,_name(name)
不如 std::string name
+ _name(std::move(name))
推荐。
如果我没理解错的话,下面需要一个副本(通过构造函数)加上一个移动(从临时到成员):
Dog::Dog(std::string name) : _name(std::move(name)) {}
另一种(也是老式的)方法是通过引用传递它并复制它(从对成员的引用):
Dog::Dog(const std::string &name) : _name(name) {}
如果第一种方法需要复制和移动两者,而第二种方法只需要一个副本,那么第一种方法如何成为首选,并且在某些情况下更快?
当消费数据时,您需要一个可以消费的对象。当您获得 std::string const&
时,您 将 必须独立于是否需要参数来复制对象。
当按值传递对象时,如果必须复制对象,则对象将被复制,即当传递的对象不是临时对象时。但是,如果它恰好是临时对象,则可以就地构建对象,即,任何副本都可能已被删除,您只需支付移动构建费用。也就是说,有可能实际上没有复制发生。
考虑用左值和右值调用各种选项:
Dog::Dog(const std::string &name) : _name(name) {}
无论是用左值还是右值调用,这都需要一个副本,以从
name
初始化_name
。移动不是一种选择,因为name
是const
。Dog::Dog(std::string &&name) : _name(std::move(name)) {}
这只能用右值调用,它会移动。
Dog::Dog(std::string name) : _name(std::move(name)) {}
当使用左值调用时,这将复制以传递参数,然后移动以填充数据成员。当使用右值调用时,这将移动到传递参数,然后移动到填充数据成员。在右值的情况下,可以省略传递参数的移动。因此,用左值调用它会导致一次复制和一次移动,而用右值调用它会导致一到两次移动。
最佳解决方案是同时定义(1)
和(2)
。解决方案 (3)
可以相对于最优解有额外的移动。但是编写一个函数比编写两个几乎相同的函数更短且更易于维护,而且移动被认为是便宜的。
当使用可隐式转换为字符串的值调用时,如 const char*
,会发生隐式转换,这涉及长度计算和字符串数据的副本。然后我们陷入了右值的情况。在这种情况下,使用 string_view
提供了另一种选择:
Dog::Dog(std::string_view name) : _name(name) {}
当使用字符串左值或右值调用时,会生成一个副本。当使用
const char*
调用时,进行一次长度计算并复制一份。
首先简短回答:通过 const& 调用将始终花费一个副本。根据条件按值调用可能只花费一步 .但这取决于(请查看下面的代码示例以了解此 table 所指的场景):
lvalue rvalue unused lvalue unused rvalue
------------------------------------------------------
const& copy copy - -
rvalue&& - move - -
value copy, move move copy -
T&& copy move - -
overload copy move - -
所以我的执行摘要是如果
- 一步很便宜,因为可能会有额外的一步
- 无条件使用该参数。如果不使用参数,按值调用也会花费一个副本,例如因为 if 子句或某事。
按价值调用
考虑一个用于复制其参数的函数
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
private:
std::vector<std::string> names;
};
如果左值传递给 name_it
,在右值的情况下,您也将有两个复制操作。那很糟糕,因为我可以移动右值。
一种可能的解决方案 是为右值编写重载:
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
void name_it(std::string&& newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
这解决了问题,一切都很好,尽管您有两个代码,两个函数的代码完全相同。
另一个可行的解决方案 是使用完美转发,但这也有几个缺点,(例如完美转发函数非常贪婪并且使现有的重载 const& 函数无用,通常他们需要在头文件中,他们在目标代码中创建几个函数等等。)
class Dog {
public:
template<typename T>
void name_it(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<std::string> names;
};
另一种解决方案 是使用按值调用:
class Dog {
public:
void name_it(std::string newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
重要的是,正如你提到的std::move
。这样您将拥有一个用于右值和左值的函数。您将移动右值但接受左值的额外移动,这可能很好如果移动便宜并且您复制或移动参数而不考虑条件。
所以最后我真的认为推荐一种方式而不是其他方式是完全错误的。这在很大程度上取决于。
#include <vector>
#include <iostream>
#include <utility>
using std::cout;
class foo{
public:
//constructor
foo() {}
foo(const foo&) { cout << "\tcopy\n" ; }
foo(foo&&) { cout << "\tmove\n" ; }
};
class VDog {
public:
VDog(foo name) : _name(std::move(name)) {}
private:
foo _name;
};
class RRDog {
public:
RRDog(foo&& name) : _name(std::move(name)) {}
private:
foo _name;
};
class CRDog {
public:
CRDog(const foo& name) : _name(name) {}
private:
foo _name;
};
class PFDog {
public:
template <typename T>
PFDog(T&& name) : _name(std::forward<T>(name)) {}
private:
foo _name;
};
//
volatile int s=0;
class Dog {
public:
void name_it_cr(const foo& in_name) { names.push_back(in_name); }
void name_it_rr(foo&& in_name) { names.push_back(std::move(in_name));}
void name_it_v(foo in_name) { names.push_back(std::move(in_name)); }
template<typename T>
void name_it_ur(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<foo> names;
};
int main()
{
std::cout << "--- const& ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_cr(my_foo);
std::cout << "rvalue:";
b.name_it_cr(foo());
}
std::cout << "--- rvalue&& ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue: -\n";
std::cout << "rvalue:";
a.name_it_rr(foo());
}
std::cout << "--- value ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_v(my_foo);
std::cout << "rvalue:";
b.name_it_v(foo());
}
std::cout << "--- T&&--\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_ur(my_foo);
std::cout << "rvalue:";
b.name_it_ur(foo());
}
return 0;
}
输出:
--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move
除了性能原因之外,当副本在按值构造函数上抛出异常时,它首先抛给调用者,而不是在构造函数本身内。这使得编写 noexcept 构造函数变得更容易,而不必担心资源泄漏或构造函数上的 try/catch 块。
struct A {
std::string a;
A( ) = default;
~A( ) = default;
A( A && ) noexcept = default;
A &operator=( A && ) noexcept = default;
A( A const &other ) : a{other.a} {
throw 1;
}
A &operator=( A const &rhs ) {
if( this != &rhs ) {
a = rhs.a;
throw 1;
}
return *this;
}
};
struct B {
A a;
B( A value ) try : a { std::move( value ) }
{ std::cout << "B constructor\n"; }
catch( ... ) {
std::cerr << "Exception in B initializer\n";
}
};
struct C {
A a;
C( A const &value ) try : a { value }
{ std::cout << "C constructor\n"; }
catch( ... ) {
std::cerr << "Exception in C initializer\n";
}
};
int main( int, char ** ) {
try {
A a;
B b{a};
} catch(...) { std::cerr << "Exception outside B2\n"; }
try {
A a;
C c{a};
} catch(...) { std::cerr << "Exception outside C\n"; }
return EXIT_SUCCESS;
}
会输出
Exception outside B2
Exception in C initializer
Exception outside C
我做了一个实验:
#include <cstdio>
#include <utility>
struct Base {
Base() { id++; }
static int id;
};
int Base::id = 0;
struct Copyable : public Base {
Copyable() = default;
Copyable(const Copyable &c) { printf("Copyable [%d] is copied\n", id); }
};
struct Movable : public Base {
Movable() = default;
Movable(Movable &&m) { printf("Movable [%d] is moved\n", id); }
};
struct CopyableAndMovable : public Base {
CopyableAndMovable() = default;
CopyableAndMovable(const CopyableAndMovable &c) {
printf("CopyableAndMovable [%d] is copied\n", id);
}
CopyableAndMovable(CopyableAndMovable &&m) {
printf("CopyableAndMovable [%d] is moved\n", id);
}
};
struct TEST1 {
TEST1() = default;
TEST1(Copyable c) : q(std::move(c)) {}
TEST1(Movable c) : w(std::move(c)) {}
TEST1(CopyableAndMovable c) : e(std::move(c)) {}
Copyable q;
Movable w;
CopyableAndMovable e;
};
struct TEST2 {
TEST2() = default;
TEST2(Copyable const &c) : q(c) {}
// TEST2(Movable const &c) : w(c)) {}
TEST2(CopyableAndMovable const &c) : e(std::move(c)) {}
Copyable q;
Movable w;
CopyableAndMovable e;
};
int main() {
Copyable c1;
Movable c2;
CopyableAndMovable c3;
printf("1\n");
TEST1 z(c1);
printf("2\n");
TEST1 x(std::move(c2));
printf("3\n");
TEST1 y(c3);
printf("4\n");
TEST2 a(c1);
printf("5\n");
TEST2 s(c3);
printf("DONE\n");
return 0;
}
结果如下:
1
Copyable [4] is copied
Copyable [5] is copied
2
Movable [8] is moved
Movable [10] is moved
3
CopyableAndMovable [12] is copied
CopyableAndMovable [15] is moved
4
Copyable [16] is copied
5
CopyableAndMovable [21] is copied
DONE
结论:
template <typename T>
Dog::Dog(const T &name) : _name(name) {}
// if T is only copyable, then it will be copied once
// if T is only movable, it results in compilation error (conclusion: define separate move constructor)
// if T is both copyable and movable, it results in one copy
template <typename T>
Dog::Dog(T name) : _name(std::move(name)) {}
// if T is only copyable, then it results in 2 copies
// if T is only movable, and you called Dog(std::move(name)), it results in 2 moves
// if T is both copyable and movable, it results in one copy, then one move.