首先最重要的问题:
在C++中,有没有关于如何发送参数的最佳实践,因为我觉得这并不是一件容易的事情。
如果您的函数需要修改传递的原始对象,以便在调用返回后,对该对象的修改将对调用者可见,则应通过左值引用进行传递:
void foo(my_class& obj)
{
// Modify obj here...
}
如果您的函数不需要修改原始对象,也不需要创建它的副本(换句话说,它只需要观察其状态),那么应该通过对const
的左值引用进行传递:
void foo(my_class const& obj)
{
}
这将允许您使用左值(具有稳定标识的对象)和右值(例如临时对象或通过调用std :: move()即将移动的对象)调用函数。
对于基本类型或复制速度快的类型(例如int,bool或char),可以争论说如果函数仅需要观察值,则不需要通过引用传递,并且应优先考虑按值传递。如果不需要引用语义,那么是正确的,但是如果函数想要在某个地方存储指向该输入对象的指针,以便通过该指针进行的将来读取将看到在代码的其他部分中执行的值修改怎么办?在这种情况下,通过引用传递是正确的解决方案。
如果您的函数不需要修改原始对象,但需要存储该对象的副本(可能返回输入的转换结果而不更改输入),则可以考虑采用按值传递:
void foo(my_class obj)
{
}
调用上述函数时,传递lvalues将始终产生一个副本,传递rvalues将始终产生一个移动。如果您的函数需要在某个地方存储此对象,可以从中执行额外的移动(例如,在foo()
是需要将值存储在数据成员中的成员函数的情况下)。
如果类型为my_class
的对象移动代价昂贵,则可以考虑重载foo()
并为lvalues提供一个版本(接受对const
的lvalue引用),并为rvalues提供一个版本(接受rvalue引用):
void foo(my_class const& obj)
{
my_class copyOfObj = obj;
}
void foo(my_class&& obj)
{
my_class copyOfObj = std::move(obj);
}
上述函数实际上非常相似,事实上,您可以将它们合并为一个单一的函数:foo()
可以成为一个函数模板,并且您可以使用完美转发来确定传递的对象是移动还是复制:
template<typename C>
void foo(C&& obj)
{
my_class copyOfObj = std::forward<C>(obj);
}
您可能想通过观看 Scott Meyers 的这个演讲(请注意,他使用的“通用引用”术语是非标准的)来更多了解这个设计:
链接。
需要记住的一件事是,std::forward 对于右值引用通常会导致移动操作。因此,即使它看起来相对简单,多次转发同一个对象可能会导致问题,例如从同一个对象中移动两次!所以,请小心不要将其放入循环中,并且在函数调用中不要多次转发同一个参数。
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
请注意,通常情况下,除非你有充分的理由,否则不要采用基于模板的解决方案,因为它会使你的代码更难阅读。通常情况下,你应该专注于清晰和简洁。
以上只是简单的指导方针,但大多数情况下,它们会引导你做出良好的设计决策。
关于您帖子的其余部分:
如果我重写代码为[...],那么就没有复制了,只有两次移动。
这是不正确的。首先,rvalue 引用不能绑定到 lvalue,因此只有在将类型 CreditCard 的 rvalue 传递给构造函数时,才能编译该代码。例如:
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, std::move(cc));
但是,如果你尝试这样做,它就不起作用了:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
因为
cc
是一个左值,而右值引用无法绑定到左值。此外,当将引用绑定到对象时,不会执行移动操作:它只是一个引用绑定。因此,只会有一个移动。
基于本答案第一部分提供的指南,如果您关心在按值接受
CreditCard
时生成的移动次数,可以定义两个构造函数重载,一个接受对
const
的左值引用(
CreditCard const&
),另一个接受一个右值引用(
CreditCard&&
)。
当传递一个左值时,重载决议将选择前者(在这种情况下,将执行一次复制),当传递一个右值时,重载决议将选择后者(在这种情况下,将执行一次移动)。
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
当你想实现完美转发时,通常会使用std::forward<>
。在这种情况下,你的构造函数实际上将是一个构造函数模板,看起来大致如下:
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
从某种意义上讲,这将之前展示的两种重载合并成一个函数:C
会在你传递左值的情况下被推断为CreditCard&
,由于引用折叠规则,它将导致该函数被实例化:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
这将导致一个
creditCard
的
拷贝构造,正如你所希望的那样。另一方面,当传递一个rvalue时,
C
会被推断为
CreditCard
,并且将实例化该函数:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
这将导致creditCard
进行移动构造,这正是您想要的(因为传递的值是一个右值,这意味着我们有权从中移动)。
"abc"
不是std::string
类型,也不是char */const char *
类型,而是const char[N]
类型(在这种情况下N=4,由于三个字符和一个空字符)。这是一个很好的、常见的误解需要澄清。 - chris