수색…


Copy Elision의 목적

객체를 초기화하기 위해 객체가 복사되거나 이동되는 표준 위치가 있습니다. 복사 elision (때때로 반환 값 최적화라고 함)은 컴파일러가 특정 상황에서 복사 또는 이동을 피할 수 있도록 허용하는 최적화입니다.

다음 기능을 고려하십시오.

std::string get_string()
{
  return std::string("I am a string.");
}

표준의 엄격한 표현에 따르면이 함수는 임시 std::string 초기화 한 다음 반환 값 객체에 복사 / 이동 한 다음 임시를 파기합니다. 표준은 이것이 코드가 어떻게 해석되는지 분명합니다.

Copy Elision은 C ++ 컴파일러가 임시 복사본 만들기 및 이후의 복사 / 제거 작업을 무시할 수있게하는 규칙입니다. 즉, 컴파일러는 임시에 대한 초기화 식을 가져 와서 함수의 반환 값을 직접 초기화 할 수 있습니다. 이것은 명백하게 성능을 저장합니다.

그러나 사용자에게는 두 가지 눈에 띄는 효과가 있습니다.

  1. 형식에는 호출 된 복사 / 이동 생성자가 있어야합니다. 컴파일러가 복사 / 이동을 실행 취소하더라도 형식은 여전히 ​​복사 / 이동 될 수 있어야합니다.

  2. 복사 / 이동 생성자의 부작용은 제거가 발생할 수있는 상황에서는 보장되지 않습니다. 다음을 고려하세요:

C ++ 11
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 보장

C ++ 17

일반적으로 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 번 호출 할 수있게하며 대부분의 컴파일러는 후자를 선택합니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow