我是C ++初学者,但不是编程初学者。 我正在努力学习C ++(c ++ 11),对我来说最重要的是:传递参数。
我考虑过这些简单的例子:
具有所有成员基本类型的类:
CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)
具有成员基本类型+ 1复杂类型的类:
Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)
具有成员基本类型的类+ 1个复杂类型的集合:
Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)
当我创建一个帐户时,我这样做:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc);
显然,在这种情况下,信用卡将被复制两次。 如果我将该构造函数重写为
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
将有一份副本。 如果我把它重写为
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
将有2个动作,没有副本。
我认为有时你可能想复制一些参数,有时你不想在创建那个对象时复制
我来自C#,用于引用,对我来说有点奇怪,我认为每个参数应该有2个重载,但我知道我错了。
有没有关于如何在C ++中发送参数的最佳实践,因为我真的发现它,比方说,并非琐碎。你会如何处理我上面提到的例子?
答案 0 :(得分:151)
最重要的问题首先:
有没有关于如何在C ++中发送参数的最佳实践,因为我真的找到它,让我们说,不是微不足道的
如果你的函数需要修改传递的原始对象,那么在调用返回后,调用者可以看到对该对象的修改,那么你应该通过左值引用强>:
void foo(my_class& obj)
{
// Modify obj here...
}
如果您的函数不需要修改原始对象,并且不需要创建它的副本(换句话说,它只需要观察它的状态),那么你应该通过通过左值引用const
:
void foo(my_class const& obj)
{
// Observe obj here
}
这将允许你用lvalues(lvalues是具有稳定身份的对象)和rvalues(例如,rvalues,例如 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()
为a member function that needs to store the value in a data member的情况下)。
对于类型为my_class
的对象,如果移动费用,则可以考虑重载foo()
并为左值提供一个版本(接受对{{1的左值引用)和rvalues的一个版本(接受右值引用):
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()
您可能希望通过观看this talk by Scott Meyers来了解有关此设计的更多信息(请注意,他使用的术语&#34; Universal References &#34; - 标准)。
要记住的一件事是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...
}
通常会以rmues的移动结束,所以即使看起来相对无辜,多次转发同一个对象也可能是麻烦的来源 - 例如,从同一个物体移动两次!因此,请注意不要将其置于循环中,并且不要在函数调用中多次转发相同的参数:
std::forward
另请注意,除非您有充分的理由,否则通常不会使用基于模板的解决方案,因为这会使您的代码难以阅读。 通常,您应该注重清晰度和简洁性。
以上只是简单的指导原则,但大部分时间它们都会指向您做出良好的设计决策。
关于你的剩余职位:
如果我将其重写为[...],则会有2个动作而且没有副本。
这不正确。首先,rvalue引用不能绑定到左值,因此只有在将template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
类型的右值传递给构造函数时才会编译。例如:
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))
{ }
的 copy-construction ,如您所愿。另一方面,当rvalue通过时,creditCard
将推断为C
,并且此函数将被实例化:
CreditCard
这会导致Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
的 move-construction ,这就是你想要的(因为传递的值是一个右值,这意味着我们有权从它移动)
答案 1 :(得分:11)
首先,让我更正一些细节。当您说出以下内容时:
将有2个动作,没有副本。
这是错误的。绑定到右值引用不是一个动作。只有一步。
此外,由于CreditCard
不是模板参数,std::forward<CreditCard>(creditCard)
只是表达std::move(creditCard)
的简单方式。
现在...
如果你的类型有“便宜”的举动,你可能想让自己的生活更轻松,并按价值和“std::move
沿着”获取所有内容。
Account(std::string number, float amount, CreditCard creditCard)
: number(std::move(number),
amount(amount),
creditCard(std::move(creditCard)) {}
这种做法只会产生两个动作,但是如果动作便宜,它们可能是可以接受的。
虽然我们处于这种“廉价行动”的问题,但我应该提醒你,std::string
通常是通过所谓的小字符串优化实现的,因此它的移动可能不如复制某些指针那么便宜。像往常一样,在优化问题上,无论是否重要都是要问你的探查者,而不是我。
如果你不想招致那些额外的动作该怎么办?也许它们证明太贵了,或者更糟糕的是,这些类型实际上可能无法移动,你可能会产生额外的副本。
如果只有一个有问题的参数,您可以使用T const&
和T&&
提供两个重载。这将一直绑定引用,直到实际的成员初始化,复制或移动发生。
但是,如果您有多个参数,则会导致重载次数呈指数级增长。
这是一个可以通过完美转发解决的问题。这意味着你要编写一个模板,并使用std::forward
将参数的值类别作为成员传递到最终目的地。
template <typename TString, typename TCreditCard>
Account(TString&& number, float amount, TCreditCard&& creditCard)
: number(std::forward<TString>(number),
amount(amount),
creditCard(std::forward<TCreditCard>(creditCard)) {}
答案 2 :(得分:6)
首先,std::string
与std::vector
类似,是一个非常重要的类类型。这肯定不是原始的。
如果你按值将任何大的可移动类型带入构造函数,我会std::move
将它们加入成员:
CreditCard(std::string number, float amount, CreditCard creditCard)
: number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }
这正是我建议实现构造函数的方法。它导致成员number
和creditCard
被移动构造,而不是复制构造。当你使用这个构造函数时,会有一个副本(或者移动,如果是临时的),因为对象被传递给构造函数,然后在初始化成员时移动一个。
现在让我们考虑一下这个构造函数:
Account(std::string number, float amount, CreditCard& creditCard)
: number(number), amount(amount), creditCard(creditCard)
你是对的,这将涉及creditCard
的一个副本,因为它首先通过引用传递给构造函数。但是现在你不能将const
个对象传递给构造函数(因为引用是非const
)并且你不能传递临时对象。例如,你不能这样做:
Account account("something", 10.0f, CreditCard("12345",2,2015,1001));
现在让我们考虑一下:
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
在这里,您已经显示出对右值参考和std::forward
的误解。对于某些推断类型 std::forward
,当您转发的对象被声明为T&&
时,您应该只使用T
。这里CreditCard
没有被推导出来(我假设),因此std::forward
被错误地使用了。查看universal references。
答案 3 :(得分:1)
对我来说,最重要的是:传递参数。
(*)指针可能指的是动态分配的内存,因此在可能的情况下,您应该更喜欢对指针的引用,即使引用通常最终实现为指针。
(**)“normal”表示复制构造函数(如果传递相同类型参数的对象)或普通构造函数(如果为类传递兼容类型)。例如,当您将对象作为myMethod(std::string)
传递时,如果向其传递std::string
,则将使用复制构造函数,因此您必须确保存在复制构造函数。
答案 4 :(得分:1)
我对一般情况使用了一个非常简单的规则: 使用副本进行POD(int,bool,double,...) 和const&amp;其他一切......
想要复制或不复制,方法签名没有回答,更多的是你对参数的处理方式。
struct A {
A(const std::string& aValue, const std::string& another)
: copiedValue(aValue), justARef(another) {}
std::string copiedValue;
const std::string& justARef;
};
指针精度:我几乎从不使用它们。只有优势和优势是他们可以为空或重新分配。