【问题标题】:How to pass parameters correctly?如何正确传递参数?
【发布时间】:2013-03-14 02:09:13
【问题描述】:

我是 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++ 中发送参数的最佳实践,因为我真的发现它,比方说,不是微不足道的。您将如何处理我上面介绍的示例?

【问题讨论】:

  • Meta:我不敢相信有人刚刚问了一个很好的 C++ 问题。 +1。
  • 仅供参考,std::string 是一个类,就像 CreditCard 一样,不是原始类型。
  • 由于字符串混淆,虽然不相关,但您应该知道字符串文字"abc" 不是std::string 类型,不是char */const char * 类型,而是const char[N] 类型(由于三个字符和一个空值,在这种情况下 N=4)。这是一个很好的、常见的误解。
  • @chuex:Jon Skeet 到处都是 C# 问题,更不用说 C++。

标签: c++ c++11


【解决方案1】:

最重要的问题首先:

有没有关于如何在 C++ 中发送参数的最佳实践,因为我真的找到了,比如说,不是微不足道的

如果您的函数需要修改传递的原始对象,以便调用返回后,调用者可以看到对该对象的修改,那么您应该通过左值引用传递

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

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

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

这将允许您使用左值(左值是具有稳定标识的对象)和右值(右值是,例如 temporaries,或者您将要从中移动的对象来调用该函数作为调用std::move()的结果。

也有人会争辩说,对于基本类型或复制速度很快的类型,例如intboolchar,如果满足以下条件,则无需通过引用传递函数只需要观察值,应该优先考虑传值。如果 reference semantics 不需要,那是正确的,但如果函数想在某处存储指向同一个输入对象的指针怎么办,以便将来通过该指针读取将看到已修改的值在代码的其他部分执行?在这种情况下,通过引用传递是正确的解决方案。

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

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...
}

调用上述函数在传递左值时总是会产生一份副本,而在传递右值时总是会产生一份移动。如果您的函数需要将此对象存储在某处,您可以从中执行一个额外的move(例如,在foo()a member function that needs to store the value in a data member 的情况下)。

如果 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()可以变成一个函数模板,你可以使用完美转发 strong> 用于确定是否将在内部生成正在传递的对象的移动或副本:

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...
}

您可能想通过观看 this talk by Scott Meyers 了解有关此设计的更多信息(请注意,他使用的术语“通用参考”是非标准的)。

要记住的一点是,std::forward 通常会以 move 结束右值,因此即使它看起来相对无害,多次转发同一个对象也可能是麻烦 - 例如,从同一个物体移动两次!所以注意不要把它放在一个循环中,也不要在函数调用中多次转发同一个参数:

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&amp;) 和一个采用右值引用 (CreditCard&amp;&amp;)。

重载解析会在传递左值时选择前者(在这种情况下,将执行一次复制),在传递右值时会选择后者(在这种情况下,将执行一次移动)。

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&lt;&gt; 的使用通常在您想要实现完美转发时看到。在这种情况下,你的构造函数实际上是一个构造函数模板,看起来或多或少如下

template<typename C>
Account(std::string number, float amount, C&& creditCard) 
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }

从某种意义上说,这将我之前展示的两个重载组合到一个函数中:C 将被推断为CreditCard&amp;,以防你传递一个左值,并且由于引用折叠规则,它将导致该函数被实例化:

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)) 
{ }

这将导致creditCardmove-construction,这是您想要的(因为传递的值是一个右值,这意味着我们有权从它移动)。

【讨论】:

  • 说对于非原始参数类型,总是使用带有forward的模板版本是可以的吗?
  • @JackWillson:我会说只有当你真的担心移动的性能时才应该使用模板版本。有关更多信息,请参阅this question of mine,它有一个很好的答案。一般来说,如果你不需要复制,只需要观察,请参考const。如果需要修改原始对象,将 by ref 改为 non-`const.如果您需要复制并且移动很便宜,请按价值获取然后移动。
  • 我认为第三个代码示例没有必要移动。您可以只使用obj 而不是制作本地副本以移入,不是吗?
  • @AndyProwl:你在 1 小时前得到了我的 +1,但重新阅读你的(优秀)答案,我想指出两点:a)按价值并非总是如此 创建一个副本(如果它没有被移动)。该对象可以在调用者站点上就地构建,尤其是使用 RVO/NRVO 时,它甚至比人们最初想象的更频繁地工作。 b) 请指出,在最后一个示例中,std::forward 可能只被调用一次。我见过人们将它放入循环等中,并且由于很多初学者都会看到这个答案,恕我直言,应该有一个胖的“警告!”标签来帮助他们避免这个陷阱。
  • @SteveJessop:除此之外,转发函数 (especially constructors taking one argument) 存在技术问题(不一定是问题),主要是因为它们接受任何类型的参数并且可能会击败 @987654364 @ type trait 除非它们受到适当的 SFINAE 约束——这对某些人来说可能不是微不足道的。
【解决方案2】:

首先,让我更正一些细节。当您说出以下内容时:

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

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

另外,由于CreditCard 不是模板参数,std::forward&lt;CreditCard&gt;(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&amp;T&amp;&amp;。这将一直绑定引用,直到发生复制或移动的实际成员初始化。

但是,如果您有多个参数,则会导致重载数量呈指数级增长。

这是一个可以通过完美转发来解决的问题。这意味着您改为编写一个模板,并使用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)) {}

【讨论】:

  • 模板版本有问题:用户不能再写Account("",0,{brace, initialisation})了。
  • @ipc 啊,真的。这真的很烦人,而且我认为没有简单的可扩展解决方法。
【解决方案3】:

首先,std::stringstd::vector 一样是一个非常重要的类类型。这当然不是原始的。

如果您将任何大型可移动类型按值放入构造函数中,我会 std::move 将它们放入成员中:

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

这正是我推荐实现构造函数的方式。它会导致成员 numbercreditCard 被移动构造,而不是复制构造。当您使用此构造函数时,将有一个副本(或移动,如果是临时的),因为对象被传递到构造函数,然后在初始化成员时移动。

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

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&amp;&amp; 对于某些 推导类型 T 时,您才应该真正使用 std::forward。这里CreditCard 没有被推断出来(我假设),所以std::forward 被错误地使用了。查找universal references

【讨论】:

    【解决方案4】:

    我对一般情况使用了一个非常简单的规则: 使用 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; 
    };
    

    指针精度:我几乎从未使用过它们。唯一优于 & 的是它们可以为 null,或重新分配。

    【讨论】:

    • "我对一般情况使用了一个非常简单的规则:对 POD (int, bool, double,...) 使用 copy,对其他所有内容使用 const &。”没有。只是没有。
    • 如果您想使用 &(无 const)修改值,可能会添加。否则我看不到......简单就足够了。但如果你这么说的话……
    • 有时你想通过引用原始类型来修改,有时你需要复制对象,有时你必须移动对象。您不能将所有内容都简化为 POD > 按值和 UDT > 按引用。
    • 好的,这就是我之后添加的。答案可能太快了。
    【解决方案5】:

    对我来说最重要的事情有点不清楚:传递参数。

    • 如果要修改函数/方法内部传递的变量
      • 你通过引用传递它
      • 您将其作为指针 (*) 传递
    • 如果要读取函数/方法内部传递的值/变量
      • 你通过 const 引用传递它
    • 如果要修改函数/方法内部传递的值
      • 通常通过复制对象来传递它 (**)

    (*) 指针可能指的是动态分配的内存,因此在可能的情况下,您应该更喜欢引用而不是指针,即使引用最终通常实现为指针。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-03-10
      • 2018-02-10
      • 2021-11-30
      • 2018-07-10
      • 2022-01-26
      • 2016-01-08
      相关资源
      最近更新 更多