我是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);
Run Code Online (Sandbox Code Playgroud)
显然,在这种情况下,信用卡将被复制两次.如果我重写该构造函数为
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
Run Code Online (Sandbox Code Playgroud)
会有一份副本.如果我把它重写为
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
Run Code Online (Sandbox Code Playgroud)
将有2个动作,没有副本.
我想有时您可能想要复制一些参数,有时您不希望在创建该对象时进行复制.
我来自C#,用于引用,对我来说有点奇怪,我认为每个参数应该有2个重载,但我知道我错了.
有没有关于如何在C++中发送参数的最佳实践,因为我真的发现它,比方说,并非琐碎.你会如何处理我上面提到的例子?
And*_*owl 156
最重要的问题第一:
有没有关于如何在C++中发送参数的最佳实践,因为我真的发现它,比方说,并非琐碎
如果你的函数需要修改传递的原始对象,那么在调用返回后,调用者可以看到对该对象的修改,那么你应该通过左值引用传递:
void foo(my_class& obj)
{
// Modify obj here...
}
Run Code Online (Sandbox Code Playgroud)
如果你的函数不需要修改原始对象,并且不需要创建它的副本(换句话说,它只需要观察它的状态),那么你应该通过左值引用const
传递给:
void foo(my_class const& obj)
{
// Observe obj here
}
Run Code Online (Sandbox Code Playgroud)
这将允许您使用左值(lvalues是具有稳定标识的对象)和rvalues(rvalues,例如临时值,或者您将要调用的对象移动的对象)来调用该函数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...
}
Run Code Online (Sandbox Code Playgroud)
调用上面的函数在传递左值时总是会产生一个副本,而在传递右值时会移动一个副本.如果你的函数需要在一些地方保存这个对象,可以执行额外的举措从它(例如,在的情况下foo()
是需要存储在数据成员的值的成员函数).
如果类型对象的移动很昂贵my_class
,那么您可以考虑重载foo()
并为左值(接受左值引用const
)提供一个版本,为rvalues 提供一个版本(接受右值引用):
// 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...
}
Run Code Online (Sandbox Code Playgroud)
事实上,上面的函数是如此相似,你可以用它来制作一个单独的函数: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...
}
Run Code Online (Sandbox Code Playgroud)
您可以通过观看Scott Meyers的演讲来了解有关此设计的更多信息(请注意,他使用的术语" 通用参考 "是非标准的).
要记住的一件事是,std::forward
通常最终会转向 rvalues,所以即使它看起来相对无辜,多次转发同一个对象也可能是麻烦的来源 - 例如,从同一个对象移动两次!所以注意不要把它放在循环中,也不要在函数调用中多次转发同一个参数:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
Run Code Online (Sandbox Code Playgroud)
另请注意,除非您有充分的理由,否则通常不会使用基于模板的解决方案,因为这会使您的代码难以阅读.通常,您应该注重清晰度和简洁性.
以上只是简单的指导原则,但大部分时间它们都会指向您做出良好的设计决策.
关于你的帖子:
如果我将其重写为[...],则会有2个动作而没有副本.
这是不正确的.首先,rvalue引用不能绑定到左值,因此只有在将rvalue类型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));
Run Code Online (Sandbox Code Playgroud)
但是如果你尝试这样做它将无法工作:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Run Code Online (Sandbox Code Playgroud)
因为cc
是左值和右值引用不能绑定到左值.此外,当绑定对象的引用时,不执行任何移动:它只是一个引用绑定.因此,只会有一个举动.
因此,根据本答案第一部分提供的指导原则,如果您关注采用CreditCard
by值时生成的移动数,则可以定义两个构造函数重载,一个采用左值引用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
{ }
Run Code Online (Sandbox Code Playgroud)
std::forward<>
当您想要实现完美转发时,通常会看到您的使用情况.在这种情况下,您的构造函数实际上是一个构造函数模板,并且看起来或多或少如下
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
Run Code Online (Sandbox Code Playgroud)
从某种意义上说,这将我之前已经显示的重载结合到一个单独的函数中:C
将推断出CreditCard&
以防万一传递左值,并且由于引用折叠规则,它将导致此函数被实例化:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Run Code Online (Sandbox Code Playgroud)
这将导致一个拷贝构造的creditCard
,正如你所想.另一方面,当rvalue被传递时,C
将被推断为CreditCard
,并且将实例化该函数:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Run Code Online (Sandbox Code Playgroud)
这将导致移动建设的creditCard
,这是你想要的(因为正在传递的价值是一个右值,这意味着我们被授权从它移动).
R. *_*des 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)) {}
Run Code Online (Sandbox Code Playgroud)
当它只能产生一个时,这种方法会产生两个动作,但如果动作很便宜,它们可能是可以接受的.
虽然我们正处于这种"廉价行动"的问题,但我应该提醒你,这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)) {}
Run Code Online (Sandbox Code Playgroud)
首先,std::string
就像是一个非常庞大的类型std::vector
.这当然不是原始的.
如果你按值将任何大的可移动类型带入构造函数中,我会将std::move
它们放入成员中:
CreditCard(std::string number, float amount, CreditCard creditCard)
: number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }
Run Code Online (Sandbox Code Playgroud)
这正是我建议实现构造函数的方法.它导致成员number
和creditCard
移动构造,而不是复制构造.当您使用此构造函数时,将有一个副本(或移动,如果是临时的),因为对象被传递到构造函数,然后在初始化成员时移动一个.
现在让我们考虑一下这个构造函数:
Account(std::string number, float amount, CreditCard& creditCard)
: number(number), amount(amount), creditCard(creditCard)
Run Code Online (Sandbox Code Playgroud)
你是对的,这将涉及一个副本creditCard
,因为它首先通过引用传递给构造函数.但是现在您无法将const
对象传递给构造函数(因为引用是非const
),并且您无法传递临时对象.例如,你不能这样做:
Account account("something", 10.0f, CreditCard("12345",2,2015,1001));
Run Code Online (Sandbox Code Playgroud)
现在让我们考虑一下:
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
Run Code Online (Sandbox Code Playgroud)
在这里你已经表现出对右值参考和误差的误解std::forward
.std::forward
当您转发的对象被声明T&&
为某种推断类型 时,您应该只使用它T
.这里CreditCard
没有推断(我假设),因此std::forward
错误地使用了.查找通用引用.
归档时间: |
|
查看次数: |
8461 次 |
最近记录: |