이것이 좋은 패턴 인 이유를 이해하려면 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가되어 throw
s를 본문에서 호출 범위로 이동합니다. (때때로 직접 구성을 통해 피할 수 있거나 move
던지는 위치를 제어하기 위해 항목을 구성 하고 인수에 넣을 수있는 사람).