수색…
Copy Elision의 목적
객체를 초기화하기 위해 객체가 복사되거나 이동되는 표준 위치가 있습니다. 복사 elision (때때로 반환 값 최적화라고 함)은 컴파일러가 특정 상황에서 복사 또는 이동을 피할 수 있도록 허용하는 최적화입니다.
다음 기능을 고려하십시오.
std::string get_string()
{
return std::string("I am a string.");
}
표준의 엄격한 표현에 따르면이 함수는 임시 std::string
초기화 한 다음 반환 값 객체에 복사 / 이동 한 다음 임시를 파기합니다. 표준은 이것이 코드가 어떻게 해석되는지 분명합니다.
Copy Elision은 C ++ 컴파일러가 임시 복사본 만들기 및 이후의 복사 / 제거 작업을 무시할 수있게하는 규칙입니다. 즉, 컴파일러는 임시에 대한 초기화 식을 가져 와서 함수의 반환 값을 직접 초기화 할 수 있습니다. 이것은 명백하게 성능을 저장합니다.
그러나 사용자에게는 두 가지 눈에 띄는 효과가 있습니다.
형식에는 호출 된 복사 / 이동 생성자가 있어야합니다. 컴파일러가 복사 / 이동을 실행 취소하더라도 형식은 여전히 복사 / 이동 될 수 있어야합니다.
복사 / 이동 생성자의 부작용은 제거가 발생할 수있는 상황에서는 보장되지 않습니다. 다음을 고려하세요:
struct my_type
{
my_type() = default;
my_type(const my_type &) {std::cout <<"Copying\n";}
my_type(my_type &&) {std::cout <<"Moving\n";}
};
my_type func()
{
return my_type();
}
호출 func
는 무엇을 할 것인가? 임시는 rvalue이고 my_type
은 이동 가능한 유형이므로 "복사 중"이 인쇄되지 않습니다. 그러면 "이동"이 인쇄됩니까?
사본 추출 규칙이 없으면 항상 "이동"을 인쇄해야합니다. 그러나 복사본 엘레멘트 규칙이 존재하기 때문에 이동 생성자가 호출되거나 호출되지 않을 수 있습니다. 구현에 의존합니다.
따라서 추출이 가능한 컨텍스트에서 복사 / 이동 생성자를 호출 할 수는 없습니다.
elision은 최적화이기 때문에 컴파일러는 모든 경우에 elision을 지원하지 않을 수 있습니다. 그리고 컴파일러가 특정 케이스를 생략하는지 여부에 관계없이 유형은 여전히 생략 된 작업을 지원해야합니다. 따라서 복사본 구성이 생략되면 형식을 호출하지 않아도 복사본 생성자가 있어야합니다.
Copy Elision 보장
일반적으로 elision은 최적화입니다. 거의 모든 컴파일러가 가장 단순한 경우에 elion 복사를 지원하지만, elision을 사용하면 여전히 사용자에게 특정 부담을줍니다. 즉, 복사 / 이동이 생략 된 유형 은 여전히 생략 된 복사 / 이동 작업을 가지고 있어야합니다 .
예 :
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
return std::lock_guard<std::mutex>(a_mutex);
}
이것은 a_mutex
가 어떤 시스템에 의해 비공개로 유지되는 뮤텍스이지만, 외부 사용자가 범위가 지정된 잠금을 원할 수있는 경우에 유용 할 수 있습니다.
std::lock_guard
를 복사하거나 이동할 수 없기 때문에 이것은 또한 유효하지 않습니다. 사실상 모든 C ++ 컴파일러가 복사 / 이동을 제거하지만 표준 은 해당 유형이 해당 작업을 사용할 수 있도록해야합니다.
C ++까지 17.
C ++ 17은 복사 / 이동이 발생하지 않도록 특정 표현식의 의미를 효과적으로 재정의하여 elision을 명령합니다. 위의 코드를 고려하십시오.
pre-C ++ 17 문구에서 코드는 임시를 생성 한 다음 임시를 사용하여 반환 값으로 복사 / 이동하지만 임시 복사본은 생략 할 수 있습니다. C ++ 17 문구에 따르면 임시로는 생성되지 않습니다.
C ++ 17에서 표현식 과 동일한 유형의 객체를 초기화하는 데 사용되는 prvalue 표현식 은 임시를 생성하지 않습니다. 표현식은 해당 객체를 직접 초기화합니다. 반환 값과 동일한 유형의 prvalue를 반환하면 형식에 복사 / 이동 생성자가 필요하지 않습니다. 따라서 C ++ 17 규칙에 따라 위의 코드가 작동 할 수 있습니다.
prvalue의 유형이 초기화되는 유형과 일치하는 경우 C ++ 17 표현이 작동합니다. 위의 get_lock
하면 복사 / 이동이 필요하지 않습니다.
std::lock_guard the_lock = get_lock();
get_lock
의 결과는 동일한 유형의 오브젝트를 초기화하는 데 사용되는 prvalue 표현식이므로 복사 또는 이동이 발생하지 않습니다. 그 표현은 결코 일시적이지 않다. 그것은 the_lock
을 직접 초기화하는 데 사용됩니다. 엘리트가되기위한 복사 / 이동이 없으므로 엘리트가 없습니다.
따라서 "보증 된 복사본 제거"라는 용어는 잘못된 이름이지만 C + + 17 표준화를 위해 제안 된 기능의 이름입니다 . 그것은 elision을 전혀 보장하지 않습니다. 복사 / 이동을 모두 제거 하고 C ++을 재정 의하여 복사 / 이동이 절대로 없어지지 않도록합니다.
이 기능은 prvalue 표현식을 사용하는 경우에만 작동합니다. 따라서 일반적인 엘레멘트 규칙을 사용합니다.
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
std::lock_guard<std::mutex> my_lock(a_mutex);
//Do stuff
return my_lock;
}
이것이 copy elision의 유효한 경우이지만, C ++ 17 규칙은이 경우 복사 / 이동을 제거 하지 않습니다. 따라서 형식에는 반환 값을 초기화하는 데 사용할 복사 / 이동 생성자가 있어야합니다. 그리고 lock_guard
는 그렇지 않기 때문에 이것은 여전히 컴파일 오류입니다. 구현은 쉽게 복사 할 수있는 유형의 객체를 전달하거나 반환 할 때 복사본을 삭제할 수 있습니다. 이것은 일부 ABI가 호출 규칙에서 위임 할 수있는 레지스터에서 이러한 객체를 이동할 수있게하는 것입니다.
struct trivially_copyable {
int a;
};
void foo (trivially_copyable a) {}
foo(trivially_copyable{}); //copy elision not mandated
반환 값 elision
함수에서 prvalue 표현식 을 리턴하고 prvalue 표현식 이 함수의 리턴 유형과 동일한 유형을 갖는 경우, prvalue temporary의 사본을 생략 할 수 있습니다.
std::string func()
{
return std::string("foo");
}
거의 모든 컴파일러는이 경우 임시 구성을 제거합니다.
매개 변수 제거
인수를 함수에 전달할 때 인수가 함수의 매개 변수 유형의 prvalue 표현이고이 유형이 참조가 아닌 경우 prvalue의 구성을 생략 할 수 있습니다.
void func(std::string str) { ... }
func(std::string("foo"));
이것은 임시 string
을 작성한 다음 함수 매개 변수 str
으로 이동시키는 것입니다. Copy elision은이 표현식이 임시 + 이동을 사용하지 않고 str
에 직접 객체를 생성하도록 허용합니다.
이는 생성자가 explicit
선언 된 경우에 유용합니다. 예를 들어 위의 코드는 func("foo")
로 작성할 수 있지만 string
에는 const char*
를 string
로 변환하는 암시 적 생성자가 있기 때문에 가능 string
. 해당 생성자가 explicit
이면 explicit
생성자를 호출하기 위해 임시를 사용해야합니다. 복사 Elision은 불필요한 복사 / 이동 작업을하지 않아도됩니다.
명명 된 반환 값 elision
lvalue 표현식 을 함수에서 반환하면이 lvalue :
- 해당 함수에 대해 자동으로 로컬 변수를 나타내며,
return
후에는 소멸됩니다. - 자동 변수는 함수 매개 변수가 아닙니다.
- 변수의 타입은 함수의 리턴 타입과 같은 타입이다.
이 모든 것이 사실이라면, lvalue로부터 복사 / 이동을 생략 할 수 있습니다 :
std::string func()
{
std::string str("foo");
//Do stuff
return str;
}
더 복잡한 경우에는 elision을 사용할 수 있지만 사례가 복잡할수록 컴파일러가 실제로이를 제거 할 가능성은 낮아집니다.
std::string func()
{
std::string ret("foo");
if(some_condition)
{
return "bar";
}
return ret;
}
컴파일러는 여전히 ret
할 수 있지만 그렇게하는 기회는 줄어 듭니다.
앞에서 언급했듯이 값 매개 변수 에는 elision을 사용할 수 없습니다.
std::string func(std::string str)
{
str.assign("foo");
//Do stuff
return str; //No elision possible
}
초기 복사 제거
prvalue 표현식 을 사용하여 변수 초기화를 복사하고 그 변수가 prvalue 표현식과 동일한 유형이면 복사를 생략 할 수 있습니다.
std::string str = std::string("foo");
복사 초기화는 이것을 효과적으로 std::string str("foo");
(사소한 차이가 있음).
이것은 반환 값에도 적용됩니다.
std::string func()
{
return std::string("foo");
}
std::string str = func();
copy elision이 없으면 std::string
의 move constructor을 2 번 호출하게됩니다. Copy elision은 move 생성자를 1 또는 0 번 호출 할 수있게하며 대부분의 컴파일러는 후자를 선택합니다.