如何正确传递参数?

我是一个 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);

Obviously the credit card will be copied twice in this scenario. 如果我将构造函数重写为

Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)

there will be one copy. 如果我把它改写成

Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))

将有两个动作,没有复制。

我认为有时候你可能想要复制一些参数,有时候你不想复制当你创建那个对象。
I come from C# and, being used to references, it's a bit strange to me and I think there should be 2 overloads for each parameter but I know I am wrong.
有没有在 C + + 中发送参数的最佳实践,因为我真的发现它,比方说,非常重要。你如何处理我上面提到的例子?

10843 次浏览

首先,和 std::vector一样,std::string是一个非常庞大的类类型,它当然不是原始类型。

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

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

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

Now let's consider this constructor:

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的误解。对于某些 deduced typeT来说,只有当所转发的对象被声明为 T&&时,才应该真正使用 std::forward。这里没有推导出 CreditCard(我假设) ,因此使用 std::forward是错误的。查一下 通用参考文献

首先最重要的问题是:

Are there any best practices of how to send parameters in C++ because I really find it, let's say, not trivial

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

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

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

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

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

也可以认为,如果函数只需要观察值和 passing by value should be favored,那么 对于复制速度快的基本类型或类型(如 intboolchar)就不需要通过引用传递。如果不需要 参考语义学,那么这是正确的,但是如果函数想要存储一个指向某个相同输入对象的指针,以便将来读取该指针时可以看到在代码的其他部分执行的值修改,那么该怎么办呢?在这种情况下,通过引用传递是正确的解决方案。

如果你的函数是 does not need to modify the original object, but needs to store a copy of that object(possibly to return the result of a transformation of the input without altering the 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...
}

Invoking the above function will always result in one copy when passing lvalues, and in one moves when passing rvalues. If your function needs to store this object somewhere, you could perform an additional 让开 from it (for instance, in the case foo() is a member function that needs to store the value in a data member).

如果对于类型为 my_class的对象,移动代价是昂贵的 ,那么您可以考虑重载 foo(),并为 lvalue 提供一个版本(接受对 const的 lvalue 引用) ,为 rvalue 提供一个版本(接受对 rvalue 的引用) :

// 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()可以变成一个函数 模板,你可以用 完美的转发来确定传递的对象的一个 move 或者一个拷贝是否会在内部生成:

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

您可能想通过观看 这是斯科特 · 迈尔斯的演讲来了解更多关于这个设计的信息(只要注意他使用的术语“ Universal References”是非标准的)。

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

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

还要注意的是,除非有充分的理由,否则通常不会求助于基于模板的解决方案,因为这会使代码更难阅读。通常,您应该关注清晰性和简单性.

以上只是一些简单的指导方针,但是大多数时候它们会指引你做出好的设计决策。


关于你其余的帖子:

如果我重写它为[ ... ]将有2个步骤,没有副本。

This is not correct. To begin with, an rvalue reference cannot bind to an lvalue, so this will only compile when you are passing an rvalue of type CreditCard to your constructor. For instance:

// 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是左值,而右值引用不能绑定到左值。而且,将引用绑定到对象时,不执行任何移动: 它只是一个引用绑定。因此,将只有 移动。


So based on the guidelines provided in the first part of this answer, if you are concerned with the number of moves being generated when you take a CreditCard by value, you could define two constructor overloads, one taking an lvalue reference to const (CreditCard const&) and one taking an rvalue reference (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复制建造,如您所愿。另一方面,当传递一个 rvalue 时,C将被推导为 CreditCard,而这个函数将被实例化:

Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }

这将导致 移动-建筑creditCard,这正是您想要的(因为传递的值是一个右值,这意味着我们被授权从它移动)。

我不太清楚最重要的是什么传递参数。

  • 如果您想修改函数/方法中传递的变量
    • 通过引用传递
    • you pass it as a pointer (*)
  • 如果您想读取函数/方法中传递的值/变量
    • 通过常量引用传递
  • 如果您想修改函数/方法内部传递的值
    • 通过复制对象(* *)传递 正常情况下

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

(* *) “正常”指的是复制建构子(如果你传递的对象与参数类型相同)或正常构造函数(如果你传递的是类的兼容类型)。例如,当你传递一个对象作为 ABC0时,如果传递了一个 std::string,那么复制建构子就会被使用,因此你必须确保它存在。

对于一般情况,我使用一个非常简单的规则: 将副本用于 POD (int、 bool、 double、 ...) 其他的一切..。

想要复制或不想复制,不是通过方法签名来回答的,而是通过参数的处理来回答的。

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

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

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

将有两个动作,没有复制。

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

此外,由于 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)) {}

This approach will yield you two moves when it could yield only one, but if the moves are cheap, they may be acceptable.

当我们在讨论这个“廉价移动”的问题时,我应该提醒你,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)) {}