C ++ 11 rvalue 및 이동 의미 론적 혼란 (return statement)


435

rvalue 참조를 이해하고 C ++ 11의 의미를 이동하려고합니다.

이 예제들 사이의 차이점은 무엇이며 어떤 것들은 벡터 복사를하지 않을 것입니까?

첫 번째 예

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

두 번째 예

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

세 번째 예

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

51
참조로 지역 변수를 반환하지 마십시오. rvalue 참조는 여전히 참조입니다.
fredoverflow 2019

63
이것은 예제들 사이의 의미 론적 차이를 이해하기 위해 의도적으로 의도 된 것입니다. lol
Tarantula

@FredOverflow 오래된 질문이지만 의견을 이해하는 데 잠시 시간이 걸렸습니다. # 2의 문제 std::move()는 영구적 인 "복사"를 만들 었는가하는 것입니다.
3Dave

5
@DavidLively std::move(expression)는 아무것도 만들지 않고 단순히 식을 xvalue로 캐스팅합니다. 평가 과정에서 개체가 복사되거나 이동되지 않습니다 std::move(expression).
fredoverflow

답변:


563

첫 번째 예

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

첫 번째 예는에 의해 발견 된 임시를 리턴합니다 rval_ref. 그 임시적인 수명은 rval_ref정의를 넘어 연장 되며 마치 마치 가치를 잡은 것처럼 사용할 수 있습니다. 이것은 다음과 매우 유사합니다.

const std::vector<int>& rval_ref = return_vector();

내 재 작성에서 분명히 rval_ref비 const 방식으로 사용할 수는 없습니다 .

두 번째 예

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

두 번째 예에서는 런타임 오류를 생성했습니다. rval_ref이제 tmp함수 내부에서 소멸 된 것에 대한 참조를 보유 합니다. 운 좋게도이 코드는 즉시 중단됩니다.

세 번째 예

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

세 번째 예는 첫 번째 예와 거의 같습니다. std::move에가 tmp불필요하며 실제로는 반환 값의 최적화를 억제하므로 성능 pessimization 수있다.

내가하는 일을 코딩하는 가장 좋은 방법은 다음과 같습니다.

모범 사례

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

즉 C ++ 03에서와 마찬가지로. tmpreturn 문에서 암시 적으로 rvalue로 처리됩니다. 반환 값 최적화 (복사 없음, 이동 없음)를 통해 반환되거나 컴파일러가 RVO를 수행 할 수 없다고 결정하면 vector의 이동 생성자를 사용하여 return을 수행합니다 . RVO가 수행되지 않고 리턴 된 유형에 이동 생성자가없는 경우에만 사본 생성자가 리턴에 사용됩니다.


65
컴파일러는 값으로 로컬 객체를 반환 할 때 RVO를 수행하며, 로컬 유형과 함수 반환은 동일하며 cv-qualified (const 유형은 반환하지 않음)입니다. RVO를 억제 할 수 있으므로 조건 (:?) 문으로 돌아 오지 마십시오. 로컬에 대한 참조를 반환하는 다른 함수로 로컬을 래핑하지 마십시오. 그냥 return my_local;. 여러 개의 return 문이 정상이며 RVO를 방해하지 않습니다.
Howard Hinnant

27
주의 사항 : 로컬 객체 의 멤버 를 반환 할 때는 이동이 명시 적이어야합니다.
boycy 2012

5
@NoSenseEtAl : 리턴 라인에 임시가 작성되지 않았습니다. move임시를 만들지 않습니다. lvalue를 xvalue로 캐스팅하여 사본을 만들지 않고 아무것도 만들지 않고 아무것도 파괴하지 않습니다. 이 예제는 lvalue-reference에 의해 반환되고 move리턴 라인에서를 제거하는 것과 동일한 상황 입니다.
Howard Hinnant

15
"복수의 리턴 문은 정상이며 RVO를 방해하지 않습니다": 동일한 변수 를 리턴 하는 경우에만 해당됩니다 .
중복 제거기

5
@ 중복 제거기 : 맞습니다. 의도 한대로 정확하게 말하지 않았습니다. 여러 반환 문이 RVO에서 컴파일러를 금지하지 않는다는 것을 의미했기 때문에 (구현이 불가능하더라도) 반환 식은 여전히 ​​rvalue로 간주됩니다.
Howard Hinnant

42

그들 중 누구도 복사하지 않지만 두 번째는 파괴 된 벡터를 나타냅니다. 명명 된 rvalue 참조는 거의 정규 코드에 존재하지 않습니다. C ++ 03에서 어떻게 사본을 작성했는지 기록합니다.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

지금을 제외하고는 벡터가 이동합니다. 사용자 클래스는 대부분의 경우에 그것의를 rvalue 참조를 처리하지 않습니다.


세 번째 예제가 벡터 복사를 수행 할 것입니까?
Tarantula

@Tarantula : 그것은 당신의 벡터를 파괴 할 것입니다. 깨기 전에 복사했는지 또는 복사하지 않았는지 여부는 실제로 중요하지 않습니다.
강아지

4
당신이 제안한 파열에 대한 이유는 없습니다. 로컬 rvalue 참조 변수를 rvalue에 바인딩하는 것이 좋습니다. 이 경우 임시 객체의 수명이 rvalue 참조 변수의 수명으로 연장됩니다.
fredoverflow 22

1
내가 이것을 배우고 있기 때문에 설명의 요점. 이 새로운 예에서, 벡터 tmp되지 않은 이동rval_ref있지만, 직접에 기록 rval_ref(즉 생략 복사) RVO를 사용. std::move복사 제거와 복사 제거 에는 차이가 있습니다 . A std::move에는 여전히 복사 할 일부 데이터가 포함될 수 있습니다. 벡터의 경우 실제로 복사 생성자에 새 벡터가 생성되고 데이터가 할당되지만 데이터 배열의 대부분은 포인터를 본질적으로 복사하여 복사됩니다. 사본 제거는 모든 사본의 100 %를 피합니다.
Mark Lakata

@MarkLakata 이것은 RVO가 아니라 NRVO입니다. NRVO는 C ++ 17에서도 선택 사항입니다. 적용되지 않은 경우 반환 값과 rval_ref변수는 모두 이동 생성자를 사용하여 생성 std::vector됩니다. 와 함께 또는없이 복사 생성자가 없습니다 std::move. 이 경우 명령문에서 rvaluetmp취급됩니다 . return
Daniel Langr 2013

16

간단한 대답은 정규 참조 코드와 마찬가지로 rvalue 참조 코드를 작성해야하며 정신적으로 99 %의 시간을 동일하게 취급해야한다는 것입니다. 여기에는 참조 반환에 대한 모든 이전 규칙이 포함됩니다 (즉, 로컬 변수에 대한 참조는 절대 반환하지 않습니다).

std :: forward를 활용하고 lvalue 또는 rvalue 참조를 사용하는 일반 함수를 작성할 수있는 템플리트 컨테이너 클래스를 작성하지 않는 한, 이것은 거의 사실입니다.

이동 생성자 및 이동 할당의 가장 큰 장점 중 하나는이를 정의하면 RVO (반환 값 최적화) 및 NRVO (명명 된 반환 값 최적화)가 호출되지 않은 경우 컴파일러에서이를 사용할 수 있다는 것입니다. 컨테이너 및 문자열과 같은 고가의 객체를 메소드에서 효율적으로 값으로 반환하는 데는 상당히 큰 것입니다.

rvalue 참조로 흥미로운 점은 일반적인 함수의 인수로 사용할 수도 있다는 것입니다. 이를 통해 const 참조 (const foo & other) 및 rvalue reference (foo && other) 모두에 대한 과부하가있는 컨테이너를 작성할 수 있습니다. 단순한 생성자 호출로 전달하기에 인수가 다루기 어려울지라도 여전히 수행 할 수 있습니다.

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

STL 컨테이너는 거의 모든 것 (해시 키 및 값, 벡터 삽입 등)에 대한 이동 과부하를 갖도록 업데이트되었으며 가장 많이 볼 수있는 곳입니다.

일반 함수에도 사용할 수 있으며, rvalue 참조 인수 만 제공하면 호출자가 객체를 작성하고 함수가 이동하도록 강제 할 수 있습니다. 이것은 실제로 사용하는 것보다 더 좋은 예이지만 렌더링 라이브러리에서로드 된 모든 리소스에 문자열을 할당하여 디버거에서 각 객체가 나타내는 것을 쉽게 볼 수 있습니다. 인터페이스는 다음과 같습니다.

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

그것은 '누수 추상화'의 한 형태이지만, 이미 대부분의 시간에 문자열을 만들어야한다는 사실을 이용하고 또 다른 복사를 피할 수 있습니다. 이것은 정확히 고성능 코드는 아니지만 사람들 이이 기능을 사용하지 못할 가능성의 좋은 예입니다. 이 코드는 실제로 변수가 호출의 임시 변수이거나 std :: move 호출 된 변수 여야합니다.

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

또는

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

또는

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

그러나 이것은 컴파일되지 않습니다!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

3

답변 자체 가 아니라 지침입니다. 대부분의 경우 지역 T&&변수 를 선언하는 데 의미가 없습니다 ( std::vector<int>&& rval_ref). 유형 메소드 std::move()에서 사용하려면 여전히 그들에게 있어야 foo(T&&)합니다. 이미 언급 한 문제는 당신이 그런 것을 반환하려고 할 때rval_ref 는 함수에서 함수 표준 참조 대 파괴-임시-피아 스코를 얻는다는 것입니다.

대부분 다음과 같은 패턴을 사용합니다.

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

반환 된 임시 객체에 대한 참조를 보유하지 않으므로 이동 된 객체를 사용하려는 프로그래머의 오류가 발생하지 않습니다.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

분명히 함수가 실제로 임시 객체가 아닌 객체에 T&&대한 참조 인 객체를 반환하는 경우가 있습니다 (아주 드물지만) .

RVO 관련 : 이러한 메커니즘은 일반적으로 작동하며 컴파일러는 복사를 피할 수 있지만 반환 경로가 명확하지 않은 경우 (예외, if명명 된 객체를 결정하는 조건, 아마도 다른 것들을 결합 할 수 있음) rref는 당신의 구원자입니다 비싼).


2

이 중 어느 것도 추가 복사를 수행하지 않습니다. RVO를 사용하지 않더라도 새로운 표준에 따르면 수익을 낼 때 이동 구성을 복사하는 것이 좋습니다.

로컬 변수에 대한 참조를 반환하기 때문에 두 번째 예제에서 정의되지 않은 동작이 발생한다고 생각합니다.


1

첫 번째 답변에 대한 주석에서 이미 언급했듯이 return std::move(...);구성은 지역 변수 반환 이외의 경우에 차이를 만들 수 있습니다. 다음은 멤버 객체 유무에 관계없이 멤버 객체를 반환 할 때 발생하는 상황을 문서화하는 실행 가능한 예제입니다 std::move().

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

아마도 return std::move(some_member);특정 클래스 멤버를 실제로 이동하려는 경우에만 의미가 있습니다. 예를 class C들어,struct A .

객체가 R 값인 경우에도 struct A항상에서 복사 되는 방법에 주목하십시오 . 컴파일러는 의 인스턴스가 더 이상 사용되지 않는다고 말할 방법이 없기 때문 입니다. 에서 , 컴파일러는이 정보 가지고 왜, 도착 이동을 의 인스턴스가하지 않는 상수이다.class Bclass Bclass Bstruct Aclass Cstd::move()struct Aclass C

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