如何正确传递参数?

我是一个C ++初学者,但不是编程初学者。 我正在尝试学习C ++(c ++ 11),对于我来说最重要的事情还不清楚:传递参数。

我考虑了这些简单的例子:

  • 具有所有成员基本types的类:
    CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)

  • 具有成员基本types的类+ 1复杂types:
    Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)

  • Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)

当我创build一个帐户,我这样做:

  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个动作,没有副本。

我想有时你可能想复制一些参数,有时你不想在创build该对象时进行复制。
我来自C#,用于引用,这对我来说有点奇怪,我认为每个参数应该有2个重载,但我知道我错了。
是否有任何有关如何在C ++中发送参数的最佳做法,因为我真的觉得它不是微不足道的。 你将如何处理上面介绍的例子?

最重要的问题第一:

是否有任何有关如何在C ++中发送参数的最佳做法,因为我真的觉得它不是微不足道的

如果你的函数需要修改被传递的原始对象,那么在调用返回之后,对该对象的修改对于调用者是可见的,那么你应该通过左值引用

 void foo(my_class& obj) { // Modify obj here... } 

如果你的函数不需要修改原始对象,也不需要创build它的副本 (换句话说,它只需要观察它的状态),那么你应该通过左值引用传递const

 void foo(my_class const& obj) { // Observe obj here } 

这将允许你使用左值(左值是具有稳定身份的对象)和右值(右值是,例如临时对象,或者你将要移动的对象作为调用std::move() )。

人们也可以争辩说, 对于复制速度很快的基本types或types ,如intboolchar ,如果函数只需要观察值,则不需要通过引用,而应该传递值 。 如果不需要引用语义 ,那么这是正确的,但是如果函数想要在某处存储一个指向同一个input对象的指针,那么通过该指针的未来读取将会看到已经在其他部分中执行的值修改码? 在这种情况下,通过引用是正确的解决scheme。

如果您的函数不需要修改原始对象,但需要存储该对象的副本可能返回input的转换结果而不改变input ),那么您可以考虑采取值

 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_classtypes的对象来说是昂贵的,那么你可以考虑重载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的这个演讲来了解更多关于这个devise的知识(只是在意他使用的术语“ Universal References ”是非标准的)。

有一点需要注意的是, std::forward通常以rvalues结尾,所以即使看起来比较清白,多次转发同一个对象也可能是一个麻烦来源 – 例如,从同一个对象对象两次! 所以要小心,不要把它放在一个循环中,也不要在函数调用中多次转发相同的参数:

 template<typename C> void foo(C&& obj) { bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous! } 

另外请注意,除非您有充分的理由,否则您通常不会诉诸于基于模板的解决scheme,因为这会使您的代码难以阅读。 通常,你应该把重点放在清晰和简单

以上只是简单的指导方针,但大多数时候他们会指导您做出良好的devise决策。


关于你的邮件的其余部分:

如果我将其重写为[…]将会有2个动作,并且没有副本。

这是不正确的。 首先,右值引用不能绑定到左值,所以这只会在您将typesCreditCard的右值传递给构造函数时进行编译。 例如:

 // 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时生成的移动次数,则可以定义两个构造函数重载,一个将左值引用为constCreditCard const& )和一个参考右值( CreditCard&& )。

当通过一个左值(在这种情况下,将执行一个副本)时,重载parsing将select前者,而在传递一个右值时(后者将执行一次移动)。

 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移动 ,这是你想要的(因为传递的价值是一个右值,这意味着我们有权从它移动)。

首先,让我来纠正一些细节。 当你说下面的话:

会有2个动作,没有副本。

这是错误的。 绑定到右值引用不是一个动作。 只有一个举动。

此外,由于CreditCard不是模板参数,因此std::forward<CreditCard>(creditCard)只是std::move(creditCard)

现在…

如果你的types有“便宜”的动作,你可能只想让自己的生活变得轻松一些,把所有的东西都按照价值和“ std::move along”。

 Account(std::string number, float amount, CreditCard creditCard) : number(std::move(number), amount(amount), creditCard(std::move(creditCard)) {} 

这种做法只会产生两个动作,但如果动作便宜,可能是可以接受的。

当我们谈到这个“廉价移动”的问题时,我应该提醒你std::string通常是用所谓的小string优化来实现的,所以它的移动可能不像复制一些指针那么便宜。 像往常一样,优化问题,无论是否重要,都是要问你的分析器,而不是我。

如果你不想招致这些额外的动作怎么办? 也许他们certificate过于昂贵,或者更糟的是,也许这些types实际上不能被移动,你可能会招致额外的副本。

如果只有一个有问题的参数,那么可以提供两个重载, 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)) {} 

首先, std::stringstd::vector是相当大的类types。 这当然不是原始的。

如果你将任何大的可移动types的值通过构造函数,我会std::move到成员:

 CreditCard(std::string number, float amount, CreditCard creditCard) : number(std::move(number)), amount(amount), creditCard(std::move(creditCard)) { } 

这正是我如何build议实现构造函数。 它使会员numbercreditCard被移动build造,而不是复制构build。 当你使用这个构造函数时,会有一个副本(或者移动,如果是临时的),因为对象被传递给构造函数,然后在初始化成员时移动一个副本。

现在让我们考虑这个构造函数:

 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的误解。 当你正在转发的对象被声明为T&&对于某些推导的types T时,你应该只使用std::forward 。 这里CreditCard不是推导(我假设),所以std::forward被错误地使用。 查找通用引用 。

对于一般情况,我使用了一个非常简单的规则:使用副本POD(int,bool,double,…)和const&一切…

想要复制与否,不是通过方法签名来回答,而是通过对参数做什么来回答。

 struct A { A(const std::string& aValue, const std::string& another) : copiedValue(aValue), justARef(another) {} std::string copiedValue; const std::string& justARef; }; 

指针的精度:我几乎从来没有使用过它们。 只有优势在于它们可以为空,或者重新分配。

最重要的事情是传递参数。

  • 如果你想修改在函数/方法内传递的variables
    • 你通过引用传递它
    • 你把它作为一个指针(*)
  • 如果你想读取函数/方法中传递的值/variables
    • 你通过const引用传递它
  • 如果你想修改在函数/方法内传递的值
    • 通常通过复制对象来传递它(**)

(*) 指针可能指向dynamic分配的内存,因此,如果可能的话,应该优先使用指针引用,即使引用通常是以指针的forms实现的。

(**) “通常”意味着通过复制构造函数(如果您传递相同types的参数的对象)或通过正常的构造函数(如果您传递类的兼容types)。 例如,当你传递一个对象为myMethod(std::string) ,如果一个std::string被传递给它,拷贝构造函数将被使用,因此你必须确保存在一个。