복사 한 다음 이동하는 이유는 무엇입니까?


98

누군가가 객체를 복사하여 클래스의 데이터 멤버로 옮기기로 결정한 코드를 보았습니다. 이로 인해 이동의 요점이 복사를 피하는 것이라고 생각했기 때문에 혼란 스러웠습니다. 다음은 그 예입니다.

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

내 질문은 다음과 같습니다.

  • 왜 우리는 rvalue 참조를받지 str않습니까?
  • 사본은 특히 비싸지 std::string않습니까?
  • 저자가 사본을 만들고 이동하기로 결정한 이유는 무엇입니까?
  • 언제 직접해야합니까?

나에게는 어리석은 실수처럼 보이지만 주제에 대해 더 많은 지식을 가진 사람이 그것에 대해 할 말이 있는지 알아보고 싶습니다.
Dave


처음에 링크하는 것을 잊은이 Q & A 는 주제와 관련이있을 수도 있습니다.
Andy Prowl 2013 년

답변:


97

질문에 답하기 전에 한 가지 잘못된 것 같습니다. C ++ 11에서 가치를 취하는 것이 항상 복사를 의미하지는 않습니다. 를 rvalue가 전달되면 그됩니다 이동 이 아니라 복사되지 않고 (가능한 이동 생성자가 존재하는 경우). 그리고 std::string이동 생성자가 있습니다.

C ++ 03에서와 달리 C ++ 11에서는 아래에서 설명 할 이유 때문에 값으로 매개 변수를 취하는 것이 관용적입니다. 또한 매개 변수를 허용하는 방법에 대한보다 일반적인 지침 세트는 StackOverflow에 대한 이 Q & A를 참조하십시오 .

왜 우리는 rvalue 참조를받지 str않습니까?

다음과 같이 lvalue를 전달할 수 없기 때문입니다.

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

Srvalue를 허용하는 생성자 만 있으면 위의 내용이 컴파일되지 않습니다.

사본은 특히 비싸지 std::string않습니까?

rvalue를 전달하면로 이동 되고 str결국로 이동됩니다 data. 복사가 수행되지 않습니다. 당신이 좌변을 전달하면, 다른 한편으로는, 그 좌변이 될 것입니다 복사str, 다음으로 이동 data.

요약하자면, rvalue에 대해 두 번의 이동, lvalue에 대해 하나의 복사본 및 하나의 이동입니다.

저자가 사본을 만들고 이동하기로 결정한 이유는 무엇입니까?

우선 위에서 언급했듯이 첫 번째는 항상 사본이 아닙니다. 대답은 " 효율적이고 ( std::string물체의 움직임 이 저렴 하기 때문에 ) 간단하기 때문 "입니다.

이동이 저렴하다는 가정 하에서 (여기서 SSO를 무시 함),이 디자인의 전체적인 효율성을 고려할 때 실제로 무시할 수 있습니다. 그렇게하면 lvalue에 대한 복사본이 하나 있고 (에 대한 lvalue 참조를 받아 들인 경우처럼 const) rvalue에 대한 복사본이 없습니다 (에 대한 lvalue 참조를 수락하면 여전히 복사본이 있음 const).

즉, 값으로 가져 오는 것이 lvalue const가 제공 될 때 lvalue 참조로 가져 오는 것만 큼 좋고 rvalue가 제공 될 때 더 좋습니다.

추신 : 몇 가지 맥락을 제공하기 위해 이것이 OP가 언급 하는 Q & A 라고 생각 합니다.


2
언급 할 가치가있는 것은 const T&인수 전달 을 대체하는 C ++ 11 패턴입니다 . 최악의 경우 (lvalue)는 동일하지만 임시 인 경우 임시로만 이동하면됩니다. 윈윈.
syam

3
@ user2030677 : 참조를 저장하지 않는 한 해당 사본을 둘러 볼 수 없습니다.
Benjamin Lindley

5
@ user2030677 : 누가 복사만큼 당신이 그것을 필요로 얼마나 비싼 관심 (그리고 당신은 유지하려는 경우, 할 사본 당신의 data회원)? lvalue 참조로 가져가더라도 복사본이있을 것입니다const
Andy Prowl

3
@BenjaminLindley : 예비로서 저는 " 움직임이 저렴하다는 가정하에이 디자인의 전반적인 효율성을 고려할 때 사실상 무시할 수 있습니다 ."라고 썼습니다 . 그렇습니다. 이동의 오버 헤드가있을 수 있지만 이것이 단순한 디자인을 더 효율적인 것으로 변경하는 것을 정당화하는 진정한 우려라는 증거가없는 한 무시할 수있는 것으로 간주되어야합니다.
Andy Prowl

1
@ user2030677 :하지만 완전히 다른 예입니다. 귀하의 질문에 대한 예에서 당신은 항상 사본을 보유하게됩니다 data!
Andy Prowl 2013 년

51

이것이 좋은 패턴 인 이유를 이해하려면 C ++ 03과 C ++ 11 모두에서 대안을 조사해야합니다.

우리는 다음을 취하는 C ++ 03 방법이 있습니다 std::string const&.

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

이 경우 항상 단일 복사가 수행됩니다. 원시 C 문자열에서 생성하는 경우 a std::string가 생성 된 다음 다시 복사됩니다 (두 개의 할당).

에 대한 참조를 std::string가져온 다음 로컬로 스왑 하는 C ++ 03 메서드가 있습니다 std::string.

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

이것은 "이동 시맨틱"의 C ++ 03 버전이며, swap종종 매우 저렴하게 최적화 할 수 있습니다 ( move). 또한 컨텍스트에서 분석해야합니다.

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

그리고 당신이 임시가 아닌을 형성하도록 강요 std::string한 다음 그것을 버립니다. (임시 std::string는 상수가 아닌 참조에 바인딩 할 수 없습니다). 그러나 할당은 하나만 수행됩니다. C ++ 11 버전은 a를 사용 &&하고를 사용 std::move하거나 임시로 호출해야합니다. 이렇게하려면 호출자 가 호출 외부에서 명시 적으로 복사본을 만들고 해당 복사본을 함수 또는 생성자로 이동해야합니다.

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

사용하다:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

다음으로 copy와 move:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

그런 다음 이것이 어떻게 사용되는지 조사 할 수 있습니다.

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

이 2 개의 오버로드 기술이 위의 두 가지 C ++ 03 스타일보다 적어도 효율적이라는 것은 분명합니다. 저는이 2- 오버로드 버전을 "가장 최적"버전이라고 부를 것입니다.

이제 복사 버전을 살펴 보겠습니다.

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

각 시나리오에서 :

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

이 버전을 "최적의"버전과 나란히 비교하면 정확히 하나의 추가 작업을 수행합니다 move. 한 번도 추가 copy.

따라서 이것이 move저렴 하다고 가정하면 이 버전은 가장 최적화 된 버전과 거의 동일한 성능을 제공하지만 코드는 2 배 더 적습니다.

그리고 2 ~ 10 개의 인수를 취한다면 코드의 감소는 지수 적입니다. 1 개의 인수로 2 배, 2로 4 배, 3으로 8 배, 4로 16 배, 10 개로 1024 배로 줄어 듭니다.

이제 완벽한 전달 및 SFINAE를 통해이 문제를 해결할 수 있습니다. 인수 10 개를 사용하는 단일 생성자 또는 함수 템플릿을 작성하고, 인수가 적절한 유형인지 확인하기 위해 SFINAE를 수행 한 다음 인수를 이동하거나 복사합니다. 필요에 따라 지역 주. 이렇게하면 프로그램 크기 문제가 수천 배 증가하는 것을 방지 할 수 있지만이 템플릿에서 생성 된 전체 함수 더미가 여전히있을 수 있습니다. (템플릿 함수 인스턴스화는 함수를 생성합니다)

그리고 생성 된 함수가 많으면 실행 가능한 코드 크기가 커져서 성능이 저하 될 수 있습니다.

move초의 비용으로 코드가 짧아지고 거의 동일한 성능을 얻을 수 있으며 코드를 이해하기가 더 쉽습니다.

이제 이것은 함수 (이 경우 생성자)가 호출 될 때 해당 인수의 로컬 복사본이 필요하다는 것을 알고 있기 때문에 작동합니다. 아이디어는 우리가 복사본을 만들 것이라는 것을 안다면, 우리가 우리의 인수 목록에 넣어 복사본을 만들고 있음을 호출자에게 알려야한다는 것입니다. 그런 다음 그들은 우리에게 사본을 줄 것이라는 사실을 중심으로 최적화 할 수 있습니다 (예를 들어 우리의 주장으로 이동하여).

'값으로 가져 오기'기법의 또 다른 장점은 이동 생성자가 종종 noexcept라는 것입니다. 즉, 값으로 가져와 인수에서 벗어나는 함수는 종종 noexcept가되어 throws를 본문에서 호출 범위로 이동합니다. (때때로 직접 구성을 통해 피할 수 있거나 move던지는 위치를 제어하기 위해 항목을 구성 하고 인수에 넣을 수있는 사람).


또한 우리가 복사본을 만들 것이라는 것을 안다면 컴파일러가 항상 더 잘 알고 있기 때문에 컴파일러가 그렇게하도록해야한다고 덧붙일 것입니다.
Rayniery

6
이 글을 쓴 이후로 또 다른 이점이 지적되었습니다. 복사 생성자는 종종 던질 수 있지만 이동 생성자는 종종 noexcept. 데이터를 복사하여 사용하면 함수를 만들 수 noexcept있으며 복사 구성으로 인해 함수 호출 외부에서 잠재적 인 오류 (예 : 메모리 부족)가 발생할 수 있습니다 .
Yakk-Adam Nevraumont 2014

3 오버로드 기술에서 "lvalue non-const, copy"버전이 필요한 이유는 무엇입니까? "lvalue const, copy"도 비 const 케이스를 처리하지 않습니까?
Bruno Martinez

@BrunoMartinez 우리는하지 않습니다!
Yakk-Adam Nevraumont 2014

13

이것은 아마도 의도적 인 것이며 복사 및 교체 관용구와 유사합니다 . 기본적으로 문자열이 생성자보다 먼저 복사되기 때문에 생성자 자체는 임시 문자열 str 만 교체 (이동)하므로 예외적으로 안전합니다.


복사 및 스왑 병렬의 경우 +1. 실제로 그것은 많은 유사점을 가지고 있습니다.
syam

11

이동을위한 생성자와 복사를위한 생성자를 작성하여 자신을 반복하고 싶지는 않습니다.

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

이것은 특히 여러 인수가있는 경우 많은 상용구 코드입니다. 솔루션은 불필요한 이동 비용에 대한 중복을 방지합니다. (그러나 이동 작업은 상당히 저렴해야합니다.)

경쟁 관용구는 완벽한 전달을 사용하는 것입니다.

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

템플릿 매직은 전달하는 매개 변수에 따라 이동 또는 복사를 선택합니다. 기본적으로 두 생성자가 모두 손으로 작성된 첫 번째 버전으로 확장됩니다. 배경 정보는 범용 참조 에 대한 Scott Meyer의 게시물을 참조하십시오 .

성능 측면에서 완벽한 포워딩 버전은 불필요한 이동을 방지하므로 사용자 버전보다 우수합니다. 그러나 버전이 읽고 쓰기가 더 쉽다고 주장 할 수 있습니다. 어쨌든 대부분의 상황에서 가능한 성능 영향은 중요하지 않으므로 결국 스타일 문제로 보입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.