저는 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))
{ }
이것이 내가 생성자를 구현하는 것이 좋습니다. 그것은 회원의 원인 number
과 creditCard
로 이동이 아니라 복사 건설보다는 건설했다. 이 생성자를 사용하면 개체가 생성자에 전달 될 때 하나의 복사본 (또는 임시 인 경우 이동)이 있고 멤버를 초기화 할 때 하나의 이동이 있습니다.
이제이 생성자를 생각해 봅시다.
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
되므로 하나가 있는지 확인해야합니다.