首先最重要的问题:
有没有关于如何在 C++ 中发送参数的最佳实践,因为我真的找到了,比如说,不是微不足道的
如果您的函数需要修改被传递的原始对象,以便在调用返回后,对该对象的修改将对调用者可见,那么您应该通过左值引用传递:
void foo(my_class& obj)
{
// Modify obj here...
}
如果你的函数不需要修改原始对象,也不需要创建它的副本(换句话说,它只需要观察它的状态),那么你应该通过左值引用传递给const
:
void foo(my_class const& obj)
{
// Observe obj here
}
这将允许您使用左值(左值是具有稳定标识的对象)和右值(右值是,例如temporaries或您将作为调用结果移动的对象)调用该函数std::move()
。
也有人可能会争辩说,对于基本类型或复制速度很快的类型,例如int
,bool
或char
,如果函数只需要观察值,则不需要通过引用传递,并且应该优先考虑通过值传递。如果不需要引用语义,那是正确的,但是如果函数想要在某处存储指向同一个输入对象的指针,那么将来通过该指针读取将看到已在其他部分执行的值修改。代码?在这种情况下,通过引用传递是正确的解决方案。
如果您的函数不需要修改原始对象,但需要存储该对象的副本(可能返回输入转换的结果而不更改输入),那么您可以考虑按值取:
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
调用上述函数在传递左值时总是会产生一份副本,而在传递右值时会产生一份移动。如果您的函数需要将此对象存储在某处,您可以从它执行额外的移动(例如,在这种情况下,foo()
成员函数需要将值存储在数据成员中)。
如果移动对于 类型 的对象来说很昂贵my_class
,那么您可以考虑重载foo()
并为左值提供一个版本(接受对 的左值引用const
)和为右值提供一个版本(接受右值引用):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
上面的函数非常相似,事实上,你可以用它做一个函数:foo()
可以成为一个函数模板,你可以使用完美的转发来确定传递的对象的移动或副本是否将在内部生成:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
您可能想通过观看Scott Meyers 的演讲来了解有关此设计的更多信息(请注意,他使用的“通用参考”一词是非标准的)。
要记住的一件事是,这std::forward
通常会导致右值的移动,所以即使它看起来相对无害,多次转发同一个对象可能是麻烦的根源——例如,从同一个对象移动两次!所以注意不要把它放在一个循环中,也不要在函数调用中多次转发同一个参数:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
另请注意,除非您有充分的理由,否则您通常不会求助于基于模板的解决方案,因为它会使您的代码更难阅读。通常,您应该专注于清晰和简单。
以上只是简单的指导方针,但大多数时候它们会为您指明良好的设计决策。
关于您的帖子的其余部分:
如果我将其重写为 [...] 将有 2 个动作并且没有副本。
这是不正确的。首先,右值引用不能绑定到左值,因此只有在将类型的右值传递CreditCard
给构造函数时才会编译。例如:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
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
另一方面,当传递一个右值时,将推C
导出为CreditCard
,并且将实例化此函数:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
这将导致 的移动构造,creditCard
这是您想要的(因为传递的值是一个右值,这意味着我们有权从它移动)。