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

저는 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 ” 라는 용어 가 비표준 이라는 사실을 기억하십시오 ).

한 가지 명심해야 std::forward할 점 은 일반적으로 rvalue 에 대한 이동 으로 끝나기 때문에 상대적으로 무해 해 보이지만 동일한 객체를 여러 번 전달하면 문제의 원인이 될 수 있습니다. 따라서 이것을 루프에 넣지 말고 함수 호출에서 동일한 인수를 여러 번 전달하지 않도록주의하십시오.

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

또한 합당한 이유가없는 한 일반적으로 템플릿 기반 솔루션에 의존하지 않습니다. 코드를 읽기 어렵게 만들기 때문입니다. 일반적으로 명확성과 단순성에 중점을 두어야합니다 .

위의 내용은 단순한 지침 일 뿐이지 만 대부분의 경우 좋은 디자인 결정을 내릴 수 있습니다.


게시물의 나머지 부분에 대해 :

내가 그것을 […]로 다시 쓰면 2 개의 움직임과 사본이 없을 것입니다.

이것은 올바르지 않습니다. 우선 rvalue 참조는 lvalue에 바인딩 할 수 없으므로 CreditCard생성자 에 유형의 rvalue를 전달할 때만 컴파일됩니다 . 예를 들면 :

// 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좌변과를 rvalue 참조 lvalues에 바인딩 할 수 없습니다. 또한 객체에 대한 참조를 바인딩 할 때 이동이 수행되지 않습니다 . 단지 참조 바인딩 일뿐입니다. 따라서 이동 은 하나 뿐입니다 .


당신은 당신이 걸릴 때 움직임의 수를 발생하는 것을 우려하는 경우 그래서이 응답의 첫번째 부분에 제공된 지침에 기초하여, CreditCard값, 두 생성자 과부하 하나 좌변 참조 복용 정의 할 수있다 const( CreditCard const&) 한 복용 rvalue 참조 ( CreditCard&&).

과부하 해결은 lvalue를 전달할 때 전자를 선택하고 (이 경우 하나의 복사가 수행됨) rvalue를 전달할 때 후자가 선택됩니다 (이 경우 하나의 이동이 수행됨).

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

어떤 의미에서 이것은 이전에 보여준 오버로드를 하나의 단일 함수로 결합합니다. lvalue를 전달하는 경우 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(전달되는 값이를 rvalue이며, 그 수단은 우리가 그것을 이동할 수있는 권한이 있기 때문에) 당신이 원하는이다.


답변

먼저 몇 가지 세부 사항을 수정하겠습니다. 다음과 같이 말할 때 :

2 개의 이동이 있고 사본은 없습니다.

그것은 거짓입니다. rvalue 참조에 대한 바인딩은 이동이 아닙니다. 이동은 하나뿐입니다.

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

이 접근 방식은 단 하나만 산출 할 수있을 때 두 가지 이동을 산출하지만 이동이 저렴하면 허용 할 수 있습니다.

우리가이 “저렴한 움직임”문제를 다루는 동안, 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)) {}


답변

우선 std::string.NET과 같은 상당히 무거운 클래스 유형 std::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생성자에 객체를 전달할 수 없으며 (참조가 non-이기 때문에 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))

여기에서 rvalue 참조 및 std::forward. std::forward전달하려는 객체가 추론 된 유형에T&& 대해 선언 된 경우 에만 실제로 사용해야 합니다 T . 여기 CreditCard에 추론되지 않았으므로 (가정하고 있습니다) std::forward오류로 사용됩니다. 조회 보편적 참조 .


답변

일반적인 경우에는 아주 간단한 규칙을 사용합니다. POD (int, bool, double, …)에는 copy를 사용하고 다른 모든 경우에는 const &를 사용합니다.

그리고 복사를 원하든 원하지 않든 메소드 서명이 아니라 매개 변수로 수행하는 작업에 더 많이 응답합니다.

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

포인터 정밀도 : 거의 사용하지 않았습니다. &보다 장점은 null이거나 재 할당 될 수 있다는 것입니다.


답변

내게 가장 중요한 것은 매개 변수 전달이라는 것이 다소 불분명합니다.

  • 함수 / 메서드 내부에 전달 된 변수를 수정하려는 경우
    • 당신은 그것을 참조로 전달
    • 포인터 (*)로 전달합니다.
  • 함수 / 메서드 내부에 전달 된 값 / 변수를 읽으려면
    • 당신은 그것을 const 참조로 전달합니다.
  • 함수 / 메서드 내부에 전달 된 값을 수정하려는 경우
    • 객체 (**)를 복사하여 정상적으로 전달합니다.

(*) 포인터는 동적으로 할당 된 메모리를 참조 할 수 있으므로 참조가 결국 일반적으로 포인터로 구현 되더라도 가능하면 포인터보다 참조를 선호해야합니다.

(**) “보통”은 복사 생성자 (동일한 매개 변수 유형의 객체를 전달하는 경우) 또는 일반 생성자 (클래스에 대해 호환 가능한 유형을 전달하는 경우)를 의미합니다. myMethod(std::string)예를 들어 객체를으로 전달할 때이 객체에 전달되면 복사 생성자가 사용 std::string되므로 하나가 있는지 확인해야합니다.


답변