program tip

매개 변수를 올바르게 전달하는 방법은 무엇입니까?

radiobox 2020. 8. 12. 08:04
반응형

매개 변수를 올바르게 전달하는 방법은 무엇입니까?


저는 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 ++에서 매개 변수를 보내는 방법에 대한 모범 사례가 있습니까? 위에 제시된 예를 어떻게 처리 하시겠습니까?


가장 중요한 질문 우선 :

C ++로 매개 변수를 보내는 방법에 대한 모범 사례가 있습니까?

함수 가 전달되는 원래 객체 수정 해야하는 경우 호출이 반환 된 후 해당 객체에 대한 수정 사항이 호출자에게 표시되도록하려면 lvalue 참조로 전달해야합니다 .

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

함수 가 원본 객체를 수정할 필요가없고 복사본을 만들 필요가없는 경우 (즉, 상태를 관찰하기 만하면const 됨) lvalue 참조를에 전달해야 합니다 .

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

이렇게하면 lvalue (lvalue는 안정적인 ID를 가진 객체)와 rvalue (예 : 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...
}

위의 함수를 호출하면 lvalue를 전달할 때 항상 하나의 사본이 생성되고 rvalue를 전달할 때 하나가 이동합니다. 함수가이 오브젝트 어딘가에 저장해야하는 경우, 당신은 추가로 수행 할 수 이동 그것에서을 (예를 들어, 경우 foo()입니다 필요가 데이터 멤버의 값을 저장하는 멤버 함수 ).

이동이 유형의 객체에 대해 비용이 많이 드는 경우my_class 오버로딩을 고려 foo()하고 lvalue에 대해 하나의 버전 (에 대한 lvalue 참조 허용 const)과 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()함수 템플릿 이 될 수 있으며 전달 되는 개체의 이동 또는 복사본이 내부적으로 생성되는지 여부를 결정 하는 데 완벽한 전달사용할 수 있습니다 .

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의이 강연을보고이 디자인에 대해 더 자세히 알아보고 싶을 수 있습니다 ( 그가 사용하는 " Universal References " 라는 용어 가 비표준 이라는 사실을 기억하십시오 ).

One thing to keep in mind is that std::forward will usually end up in a move for rvalues, so even though it looks relatively innocent, forwarding the same object multiple times may be a source of troubles - for instance, moving from the same object twice! So be careful not to put this in a loop, and not to forward the same argument multiple times in a function call:

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

Also notice, that you normally do not resort to the template-based solution unless you have a good reason for it, as it makes your code harder to read. Normally, you should focus on clarity and simplicity.

The above are just simple guidelines, but most of the time they will point you towards good design decisions.


CONCERNING THE REST OF YOUR POST:

If i rewrite it as [...] there will be 2 moves and no copy.

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));

But it won't work if you try to do this:

CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue

Because cc is an lvalue and rvalue references cannot bind to lvalues. Moreover, when binding a reference to an object, no move is performed: it's just a reference binding. Thus, there will only be one move.


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&&).

Overload resolution will select the former when passing an lvalue (in this case, one copy will be performed) and the latter when passing an rvalue (in this case, one move will be performed).

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

Your usage of std::forward<> is normally seen when you want to achieve perfect forwarding. In that case, your constructor would actually be a constructor template, and would look more or less as follows

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

In a sense, this combines both the overloads I've shown previously into one single function: C will be deduced to be CreditCard& in case you are passing an lvalue, and due to the reference collapsing rules, it will cause this function to be instantiated:

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

This will cause a copy-construction of creditCard, as you would wish. On the other hand, when an rvalue is passed, C will be deduced to be CreditCard, and this function will be instantiated instead:

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

This will cause a move-construction of creditCard, which is what you want (because the value being passed is an rvalue, and that means we are authorized to move from it).


First, let me correct some details. When you say the following:

there will be 2 moves and no copy.

That is false. Binding to an rvalue reference is not a move. There is only one move.

Additionally, since CreditCard is not a template parameter, std::forward<CreditCard>(creditCard) is just a verbose way of saying std::move(creditCard).

Now...

If your types have "cheap" moves, you may want to just make your life easy and take everything by value and "std::move along".

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.

While we are on this "cheap moves" matter, I should remind you that std::string is often implemented with the so-called small string optimisation, so its moves may not be as cheap as copying some pointers. As usual with optimisation issues, whether it matters or not is something to ask your profiler, not me.

What to do if you don't want to incur those extra moves? Maybe they prove too expensive, or worse, maybe the types cannot actually be moved and you might incur extra copies.

If there is only one problematic parameter, you can provide two overloads, with T const& and T&&. That will bind references all the time until the actual member initialisation, where a copy or move happens.

However, if you have more than one parameter, this leads to an exponential explosion in the number of overloads.

This is a problem that can be solved with perfect forwarding. That means you write a template instead, and use std::forward to carry along the value category of the arguments to their final destination as members.

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

First of all, std::string is quite a hefty class type just like std::vector. It's certainly not primitive.

If you're taking any large moveable types by value into a constructor, I would std::move them into the member:

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

This is exactly how I would recommend implementing the constructor. It causes the members number and creditCard to be move constructed, rather than copy constructed. When you use this constructor, there will be one copy (or move, if temporary) as the object is passed into the constructor and then one move when initialising the member.

Now let's consider this constructor:

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

You're right, this will involve one copy of creditCard, because it is first passed to the constructor by reference. But now you can't pass const objects to the constructor (because the reference is non-const) and you can't pass temporary objects. For example, you couldn't do this:

Account account("something", 10.0f, CreditCard("12345",2,2015,1001));

Now let's consider:

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

Here you've shown a misunderstanding of rvalue references and std::forward. You should only really be using std::forward when the object you're forwarding is declared as T&& for some deduced type T. Here CreditCard is not deduced (I'm assuming), and so the std::forward is being used in error. Look up universal references.


I use a quite simple rule for general case: Use copy for POD (int, bool, double,...) and const & for everything else...

And wanting to copy or not, is not answered by the method signature but more by what you do with the paramaters.

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

precision for pointer : I almost never used them. Only advantage over & is that they can be null, or re-assigned.


It's kinda unclear for me the most important thing: passing parameters.

  • If you want to modify the variable passed inside the function / method
    • you pass it by reference
    • you pass it as a pointer (*)
  • If you want to read the value / variable passed inside the function / method
    • you pass it by const reference
  • If you want to modify the value passed inside the function / method
    • you pass it normally by copying the object (**)

(*) pointers may refers to dynamically allocated memory, therefore when possible you should prefer references over pointers even if references are, in the end, usually implemented as pointers.

(**) "normally" means by copy constructor (if you pass an object of the same type of the parameter) or by normal constructor (if you pass a compatible type for the class). When you pass an object as myMethod(std::string), for example, the copy constructor will be used if an std::string is passed to it, therefore you have to make sure that one exists.

참고URL : https://stackoverflow.com/questions/15600499/how-to-pass-parameters-correctly

반응형