이동 의미 란 무엇입니까?


1701

C ++ 0x 에 관한 Scott Meyers와의 소프트웨어 엔지니어링 라디오 팟 캐스트 인터뷰 를 막 끝냈습니다 . 대부분의 새로운 기능은 제게 이해가되었으며, 실제로는 C ++ 0x를 제외하고는 정말 흥분됩니다. 나는 여전히 이동 의미를 얻지 못한다 ... 정확히 무엇입니까?


20
C와 C ++에서 lvalues와 rvalues에 대해 [Eli Bendersky의 블로그 기사] ( eli.thegreenplace.net/2011/12/15/… )가 꽤 유익했습니다. 또한 C ++ 11에서 rvalue 참조를 언급하고 작은 예제를 통해 소개합니다.
Nils

16
주제에 대한 Alex Allain의 설명 은 매우 잘 쓰여져 있습니다.
Patrick Sanan

19
매년 C ++에서 "새로운"이동 의미가 무엇인지 궁금합니다. Google에서이 페이지로 이동합니다. 나는 반응을 읽었고, 내 뇌는 멈췄다. 나는 C로 돌아가서 모든 것을 잊었다! 교착 상태입니다.
sky

7
@sky std :: vector <> ... 고려해 보면 어딘가에 배열에 대한 포인터가 있습니다. 이 객체를 복사하면 새 버퍼를 할당해야하고 버퍼의 데이터를 새 버퍼로 복사해야합니다. 단순히 포인터를 훔치는 것이 좋은 상황이 있습니까? 컴파일러가 객체가 일시적임을 알고있을 때 대답은 예입니다. 이동 의미론을 사용하면 컴파일러가 이동하는 객체가 사라질 것이라는 것을 컴파일러가 알고있을 때 클래스 내장이 다른 객체로 이동 및 드롭되는 방법을 정의 할 수 있습니다.
dicroce

내가 이해할 수있는 유일한 참조 : learncpp.com/cpp-tutorial/… , 즉 이동 의미의 원래 추론은 스마트 포인터에서 온 것입니다.
jw_

답변:


2480

예제 코드로 이동 의미를 이해하는 것이 가장 쉽다는 것을 알았습니다. 힙 할당 메모리 블록에 대한 포인터 만 보유하는 매우 간단한 문자열 클래스부터 시작해 보겠습니다.

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

우리는 스스로 메모리를 관리하기로 결정했기 때문에 3규칙 을 따라야합니다 . 할당 연산자 작성을 연기하고 현재로서는 소멸자와 복사 생성자 만 구현하려고합니다.

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

복사 생성자는 문자열 객체를 복사하는 의미를 정의합니다. 이 매개 변수 const string& that는 문자열 유형의 모든 표현식에 바인드되어 다음 예제에서 복사 할 수 있습니다.

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

이제 이동 의미론에 대한 주요 통찰력이 제공됩니다. 우리가 복사하는 첫 번째 줄에서만 x이 딥 카피가 실제로 필요합니다. x나중에 검사를 원할 수도 있고 x어떻게 든 변경 되면 매우 놀랄 것 입니다. 내가 방금 x세 번 (이 문장을 포함하면 네 번) 어떻게 말했고 매번 똑같은 물건을 의미 했습니까? x"lvalues" 와 같은 표현식을 호출합니다 .

2 번째 줄과 3 번째 줄의 인수는 lvalue가 아니라 rvalue입니다. 기본 문자열 객체에는 이름이 없기 때문에 클라이언트는 나중에 다시 검사 할 방법이 없습니다. rvalue는 다음 세미콜론에서 파괴되는 임시 객체를 나타냅니다 (정확하게 말하면 r 식을 포함하는 전체 표현의 끝에서). band 의 초기화 중에 c소스 문자열로 원하는 것을 할 수 있고 클라이언트가 차이점을 알 수 없기 때문에 이것은 중요합니다 !

C ++ 0x는 무엇보다도 함수 오버로딩을 통해 rvalue 인수를 감지 할 수있는 "rvalue reference"라는 새로운 메커니즘을 도입했습니다. rvalue 참조 매개 변수를 사용하여 생성자를 작성하기 만하면됩니다. 그 생성자 내부에서 우리가 할 수있는 우리가 원하는 무엇이든 우리가 그것을두고만큼으로, 소스와 함께 몇 가지 유효한 상태 :

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

우리는 여기서 무엇을 했습니까? 힙 데이터를 깊이 복사하는 대신 포인터를 복사 한 다음 소스 포인터의 소멸자에서 'delete []'가 '단지 도난당한 데이터'를 공개하지 못하도록 원래 포인터를 null로 설정했습니다. 실제로 소스 문자열에 원래 있던 데이터를 "도난"했습니다. 다시 한 번 핵심 통찰력은 어떤 상황에서도 클라이언트가 소스가 수정되었음을 감지 할 수 없다는 것입니다. 여기서는 실제로 사본을 만들지 않기 때문에이 생성자를 "이동 생성자"라고합니다. 그 역할은 자원을 복사하는 대신 한 오브젝트에서 다른 오브젝트로 자원을 이동시키는 것입니다.

축하합니다. 이제 이동 의미의 기본을 이해했습니다! 할당 연산자를 구현하여 계속합시다. copy and swap 관용구에 익숙하지 않은 경우 예외 안전과 관련된 멋진 C ++ 관용구이기 때문에 그것을 배우고 다시 오십시오.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

허, 그게 다야? "rvalue 참조는 어디에 있습니까?" 당신은 요청할 수 있습니다. "여기 필요하지 않습니다!" 내 대답은 :)

매개 변수 that 를 value로 전달 하므로 that다른 문자열 객체와 마찬가지로 초기화해야합니다. 정확히 어떻게 that초기화됩니까? 예전의 C ++ 98 에서는 "복사 생성자에 의한"대답이있었습니다. C ++ 0x에서 컴파일러는 할당 연산자에 대한 인수가 lvalue인지 rvalue인지에 따라 복사 생성자와 이동 생성자 중에서 선택합니다.

따라서이라고 a = b하면 복사 생성자 가 초기화되고 that(표현식 b이 lvalue 이기 때문에 ) 할당 연산자는 컨텐츠를 새로 작성된 깊은 사본으로 교체합니다. 이것이 바로 사본 및 교환 관용구의 정의입니다. 사본을 만들고 내용을 사본으로 바꾸고 범위를 벗어나면 사본을 제거하십시오. 여기에 새로운 것은 없습니다.

당신이 말하는 그러나 만약 a = x + y이동 생성자는 초기화됩니다 that(표현식이 때문에 x + y를 rvalue입니다), 그래서 반군 깊은 복사, 오직 효율적인 움직임은 없다. that여전히 인수와 독립적 인 객체이지만 힙 데이터를 복사 할 필요가 없으므로 이동하기 때문에 구성이 간단했습니다. x + yrvalue 이므로 복사 할 필요가 없으며 rvalue로 표시된 문자열 객체에서 이동해도됩니다.

요약하면 소스는 그대로 유지해야하므로 복사 생성자는 딥 카피를 만듭니다. 반면에 이동 생성자는 포인터를 복사 한 다음 소스의 포인터를 null로 설정할 수 있습니다. 클라이언트가 객체를 다시 검사 할 방법이 없기 때문에 이런 방식으로 소스 객체를 "무효화"해도됩니다.

이 예제가 주요 요점을 갖기를 바랍니다. 참조를 rvalue하고 의미를 이동시키는 데 더 많은 것이 있습니다. 더 자세한 내용을 원하면 제 보충 답변을 참조하십시오 .


40
@하지만 ctor가 rvalue를 가져 와서 나중에 사용할 수없는 경우 왜 일관되고 안전한 상태로 두어야합니까? that.data = 0을 설정하는 대신 왜 그대로 두지 않겠습니까?
einpoklum

70
@einpoklum가 없으면 that.data = 0문자가 너무 일찍 (일시적으로 죽을 때), 두 번 파괴됩니다. 데이터를 도용하고 공유하지 마십시오!
fredoverflow

19
@einpoklum 정기적으로 예약 된 소멸자가 계속 실행되므로 소스 객체의 이동 후 상태가 충돌을 일으키지 않도록해야합니다. 더 나은 방법으로, 소스 오브젝트가 지정 또는 기타 쓰기의 수신자가 될 수 있는지 확인해야합니다.
CTMacUser

12
@pranitkothari 그렇습니다. 모든 객체는 파괴되어야하며 심지어 객체로부터 이동해야합니다. 그리고 char 배열이 삭제되는 것을 원하지 않기 때문에 포인터를 null로 설정해야합니다.
fredoverflow

7
delete[]nullptr의 @ Virus721 은 C ++ 표준에 의해 no-op로 정의됩니다.
fredoverflow

1057

첫 번째 대답은 의미 체계를 이동하는 매우 간단한 소개였으며 단순하게 유지하기 위해 많은 세부 사항이 생략되었습니다. 그러나 의미론을 옮기는 것이 훨씬 더 많으며 두 번째 대답이 차이를 메울 시간이라고 생각했습니다. 첫 번째 대답은 이미 오래되었지만 완전히 다른 텍스트로 간단히 바꾸는 것이 옳지 않았습니다. 나는 그것이 여전히 첫 번째 소개 역할을한다고 생각합니다. 그러나 더 깊이 파고 싶다면 다음을 읽으십시오. :)

Stephan T. Lavavej는 귀중한 피드백을 제공하기 위해 시간을 보냈습니다. 스테판 감사합니다!

소개

이동 의미론은 특정 조건에서 객체가 다른 객체의 외부 리소스를 소유 할 수 있도록합니다. 이것은 두 가지 방식으로 중요합니다.

  1. 값 비싼 사본을 저렴한 가격으로 전환 예를 들어 첫 번째 답변을 참조하십시오. 개체가 하나 이상의 외부 리소스를 직접 또는 간접적으로 구성원 개체를 통해 관리하지 않는 경우 이동 의미론은 복사 의미론보다 이점을 제공하지 않습니다. 이 경우 객체를 복사하고 이동하는 것은 똑같은 의미입니다.

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. 안전한 "이동 전용"유형 구현 즉, 복사가 의미가 없지만 이동하는 유형입니다. 고유 소유권 의미론을 가진 잠금, 파일 핸들 및 스마트 포인터가 그 예입니다. 참고 :이 답변 std::auto_ptr은 더 이상 사용되지 않는 C ++ 98 표준 라이브러리 템플릿 인 std::unique_ptrC ++ 11 로 대체되었습니다 . 중급 C ++ 프로그래머는 아마도 어느 정도 친숙 할 것입니다 std::auto_ptr. 그리고 "이동 의미론"이 표시되기 때문에 C ++ 11에서 이동 의미론을 논의하기에 좋은 출발점처럼 보입니다. YMMV.

이동이란 무엇입니까?

C ++ 98 표준 라이브러리는라는 고유 한 소유권 의미론을 갖춘 스마트 포인터를 제공합니다 std::auto_ptr<T>. 에 익숙하지 않은 경우 auto_ptr예외가 발생하더라도 동적으로 할당 된 객체가 항상 해제되도록합니다.

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

특이한 점은 auto_ptr"복사"동작입니다.

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

bwith로 초기화 하면 삼각형이 복사 a되지 않고 대신 삼각형의 소유권이에서 (으)로 이전 a됩니다 b. 우리는 또한 "말 a됩니다 로 이동 b 또는"삼각형은 " 이동 에서 a b ". 삼각형 자체가 항상 메모리의 동일한 위치에 있기 때문에 혼란 스러울 수 있습니다.

개체를 이동한다는 것은 관리하는 일부 리소스의 소유권을 다른 개체로 이전하는 것을 의미합니다.

의 복사 생성자는 auto_ptr아마도 다음과 같이 보일 것입니다 (약간 단순화 됨).

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

위험하고 무해한 움직임

중요한 것은 auto_ptr구문 적으로 사본처럼 보이는 것이 실제로는 이동이라는 것입니다. 이동 된 위치에서 멤버 함수를 호출하려고하면 auto_ptr정의되지 않은 동작이 호출되므로 다음 auto_ptr에서 이동 한 후 를 사용하지 않도록주의해야합니다 .

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

그러나 항상 위험한 auto_ptr것은 아닙니다 . 팩토리 함수는 다음에 대한 완벽한 사용 사례입니다 .auto_ptr

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

두 예제 모두 동일한 구문 패턴을 따르는 방법에 유의하십시오.

auto_ptr<Shape> variable(expression);
double area = expression->area();

그러나 그중 하나는 정의되지 않은 동작을 호출하는 반면 다른 하나는 정의되지 않은 동작을 호출합니다. 그래서 표현 a과 의 차이점은 무엇 make_triangle()입니까? 둘 다 같은 유형이 아닙니까? 실제로, 그들은 서로 다른 가치 범주 를 가지고 있습니다 .

가치 범주

물론, 표현 사이에 심오한 차이가있을 수 있어야합니다 a의미 auto_ptr변수와 식 make_triangle()을 반환하는 함수의 호출을 의미 auto_ptr하므로 신선한 일시적으로 생성 값에 의해 auto_ptr객체가 호출 될 때마다. a의 일례이다 좌변 반면 make_triangle()의 일례이다 r- 수치 .

a나중에 a정의되지 않은 동작 을 호출하여 멤버 함수를 호출하려고 할 수 있기 때문에 lvalues와 같은 l 위험 요소 에서 이동하는 것은 위험 합니다. 반면에 make_triangle()복사 생성자가 작업을 수행 한 후에는 임시 값을 다시 사용할 수 없으므로 rvalue에서 이동하는 것이 안전합니다. 상기 임시를 나타내는 표현이 없다; 단순히 make_triangle()다시 쓰면 다른 임시 값을 얻게 됩니다. 실제로 이동 한 임시 파일은 이미 다음 줄에 있습니다.

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

글자 lr과제의 왼쪽과 오른쪽에 역사적인 기원이 있습니다. 할당 왼쪽에 표시 할 수없는 lvalue (배열 연산자가없는 배열 또는 사용자 정의 형식 등)가있을 수있는 rvalue (클래스 유형의 모든 rvalue)가 있으므로 C ++에서는 더 이상 사실이 아닙니다. 할당 연산자와 함께).

클래스 유형의 rvalue는 평가가 임시 오브젝트를 작성하는 표현식입니다. 정상적인 상황에서 같은 범위 내에있는 다른 식은 동일한 임시 개체를 나타내지 않습니다.

Rvalue 참조

이제 우리는 lvalues에서 이동하는 것이 잠재적으로 위험하지만 rvalues에서 이동하는 것은 무해하다는 것을 이해합니다. C ++에서 lvalue 인수와 rvalue 인수를 구별하는 언어 지원이있는 경우 lvalue에서 이동하는 것을 완전히 금지하거나 적어도 콜 사이트 에서 lvalue에서 명시 적으로 이동하여 실수로 더 이상 이동하지 않도록 할 수 있습니다.

이 문제에 대한 C ++ 11의 대답은 rvalue reference 입니다. rvalue 참조는 rvalue에만 바인딩하는 새로운 종류의 참조이며 구문은 X&&입니다. 좋은 오래된 참조 X&는 이제 lvalue 참조 로 알려져 있습니다. (참고 X&&있다 되지 기준 참조; 그러한 것은 C에 존재하지 ++).

우리 const가 믹스에 들어가면, 우리는 이미 네 가지 다른 종류의 레퍼런스를 가지고 있습니다. 어떤 유형의 표현을 X바인딩 할 수 있습니까?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

실제로, 당신은 잊어 버릴 수 있습니다 const X&&. rvalue에서 읽도록 제한되는 것은 그리 유용하지 않습니다.

rvalue 참조 X&&는 rvalue 에만 바인딩하는 새로운 종류의 참조입니다.

암시 적 변환

Rvalue 참조는 여러 버전을 거쳤습니다. 버전 2.1부터 rvalue 참조 는에서로 암시 적 변환이있는 경우 X&&다른 유형의 모든 값 범주에 바인딩 됩니다 . 이 경우 임시 유형 이 작성되고 rvalue 참조가 해당 임시에 바인드됩니다.YYXX

void some_function(std::string&& r);

some_function("hello world");

위의 예에서 "hello world"유형이 lvalue입니다 const char[12]. 거기에서 암시 적 변환이므로 const char[12]관통 const char*하는 std::string입력의 임시 std::string작성하고, r그 임시로 결합된다. 이는 rvalue (표현식)와 임시 (객체)의 구분이 약간 모호한 경우 중 하나입니다.

생성자 이동

X&&매개 변수가 있는 함수의 유용한 예 는 이동 생성자 X::X(X&& source) 입니다. 그 목적은 관리 자원의 소유권을 소스에서 현재 오브젝트로 전송하는 것입니다.

C ++ 11에서는 rvalue 참조를 사용하는 std::auto_ptr<T>것으로 대체되었습니다 std::unique_ptr<T>. 의 단순화 된 버전을 개발하고 논의 할 것입니다 unique_ptr. 첫째, 우리는 원시 포인터를 캡슐화하고 연산자를 오버로드 ->하고 *, 우리의 클래스 포인터 같은 느낌 있도록 :

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

생성자는 객체의 소유권을 가져와 소멸자가 삭제합니다.

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

이제 재미있는 생성자 인 이동 생성자가 있습니다.

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

이 이동 생성자는 auto_ptr복사 생성자가했던 것과 정확히 동일 하지만 rvalue 만 제공 할 수 있습니다.

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

두 번째 행 a은 lvalue 이므로 컴파일에 실패 하지만 매개 변수 unique_ptr&& source는 rvalue에만 바인드 될 수 있습니다. 이것이 바로 우리가 원하는 것입니다. 위험한 움직임은 절대로 암시해서는 안됩니다. make_triangle()rvalue 이기 때문에 세 번째 줄은 잘 컴파일됩니다 . 이동 생성자는 소유권을 임시에서 (으)로 이전합니다 c. 다시, 이것은 우리가 정확히 원하는 것입니다.

이동 생성자는 관리 자원의 소유권을 현재 오브젝트로 전송합니다.

할당 연산자 이동

마지막으로 누락 된 부분은 이동 할당 연산자입니다. 그 역할은 이전 리소스를 해제하고 인수에서 새 리소스를 얻는 것입니다.

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

이동 할당 연산자의이 구현이 소멸자와 이동 생성자의 논리를 어떻게 복제하는지 주목하십시오. 복사 및 교체 관용구에 익숙하십니까? 또한 이동 및 스왑 관용구로 의미를 이동하는 데 적용 할 수 있습니다.

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

이제 이것은 source유형의 변수이며 unique_ptr이동 생성자에 의해 초기화됩니다. 즉, 인수가 매개 변수로 이동됩니다. 이동 생성자 자체에는 rvalue 참조 매개 변수가 있으므로 인수는 여전히 rvalue 여야합니다. 제어 흐름의 폐쇄 브레이스에 도달하면 operator=, source자동으로 이전의 자원을 해제 범위를 벗어나.

이동 할당 연산자는 관리 자원의 소유권을 현재 오브젝트로 이전하여 이전 자원을 해제합니다. 이동 및 교체 관용구는 구현을 단순화합니다.

lvalue에서 이동

때때로 우리는 lvalue에서 벗어나고 싶어합니다. 즉, 컴파일러가 lvalue를 rvalue 인 것처럼 lvalue를 처리하여 잠재적으로 안전하지 않더라도 이동 생성자를 호출 할 수 있기를 원할 때가 있습니다. 이를 위해 C ++ 11은 std::moveheader 내부에 표준 라이브러리 함수 템플릿을 제공합니다 <utility>. std::movelvalue를 rvalue로 캐스트 하기 때문에이 이름은 약간 유감입니다 . 그것은 않습니다 하지 그 자체로 아무것도 이동합니다. 이동 만 가능 합니다. 이름이 std::cast_to_rvalue또는 이었을 수도 std::enable_move있지만 지금은 이름이 붙어 있습니다.

lvalue에서 명시 적으로 이동하는 방법은 다음과 같습니다.

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

세 번째 줄 뒤에는 a더 이상 삼각형이 없습니다. 가 있기 때문 괜찮아, 명시 적으로 작성 std::move(a), 우리는 우리의 의도를 만든 명확 : "당신이 원하는대로 친애하는 생성자을 a초기화하기 위해 c, 나는 걱정하지 않는다 a더 이상 당신의 방법을 가지고 주시기 바랍니다. a."

std::move(some_lvalue) lvalue를 rvalue로 캐스트하여 후속 이동을 가능하게합니다.

X 값

std::move(a)rvalue 인 경우에도 평가시 임시 개체가 생성 되지 않습니다 . 이 수수께끼로 인해위원회는 세 번째 가치 범주를 도입하게되었습니다. 전통적인 의미에서 rvalue가 아니지만 rvalue 참조에 바인딩 할 수있는 것을 xvalue (eXpiring value) 라고합니다 . 기존의 rvalue는 prvalue (Pure rvalue) 로 이름이 바뀌 었습니다 .

prvalue와 xvalue는 모두 rvalue입니다. X 값과 l 값은 모두 glvalue (일반화 된 l 값 )입니다. 다이어그램으로 관계를 파악하기가 더 쉽습니다.

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

xvalues만이 실제로 새롭다는 점에 유의하십시오. 나머지는 이름을 바꾸고 그룹화하기 때문입니다.

C ++ 98 rvalue는 C ++ 11에서 prvalue로 알려져 있습니다. 이전 단락에서 "rvalue"를 모두 "prvalue"로 바꿉니다.

기능에서 벗어나기

지금까지 지역 변수와 함수 매개 변수로 이동하는 것을 보았습니다. 그러나 반대 방향으로도 이동할 수 있습니다. 함수가 값으로 반환하면 호출 사이트의 일부 객체 (로컬 변수 또는 임시이지만 모든 종류의 객체 일 수 있음)는 return명령문 다음에 표현식을 이동 생성자에 대한 인수로 초기화하여 초기화됩니다 .

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

놀랍게도 자동 객체 (로 선언되지 않은 로컬 변수 static)는 함수에서 암시 적 으로 이동할 수 있습니다 .

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

이동 생성자는 lvalue result를 인수로 어떻게 받아들 입니까? 의 범위 result가 곧 끝나고 스택 해제 중에 파괴됩니다. 나중에 result어떻게 든 변한 아무도 불평 할 수 없었습니다. 제어 흐름이 호출자에게 돌아 오면 result더 이상 존재하지 않습니다! 이러한 이유로 C ++ 11에는 특별한 규칙이있어서 함수를 쓰지 않아도 함수에서 자동 객체를 반환 할 수 있습니다 std::move. 실제로 "명명 된 리턴 값 최적화"(NRVO)를 금지하므로 자동 오브젝트를 기능 밖으로 이동시키는 데 사용 해서는 안됩니다std::move .

std::move자동 개체를 기능 밖으로 이동시키는 데 사용하지 마십시오 .

두 팩토리 함수에서 리턴 유형은 rvalue 참조가 아닌 값입니다. Rvalue 참조는 여전히 참조이므로 항상 자동 개체에 대한 참조를 반환해서는 안됩니다. 컴파일러가 다음과 같이 코드를 수락하도록 속인 경우 호출자는 매달려있는 참조로 끝납니다.

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

rvalue 참조로 자동 객체를 반환하지 마십시오. 이동은 std::move단순히 rvalue를 rvalue 참조에 바인딩하는 것이 아니라 by가 아닌 이동 생성자에 의해 독점적으로 수행됩니다 .

회원으로 이동

조만간 다음과 같은 코드를 작성하게됩니다.

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

기본적으로 컴파일러는 이것이 parameterlvalue 라고 불평합니다 . 유형을 보면 rvalue 참조가 표시되지만 rvalue 참조는 단순히 "rvalue에 바인딩 된 참조"를 의미합니다. 참조 자체가 rvalue라는 의미 는 아닙니다 ! 실제로, parameter이름을 가진 일반적인 변수 일뿐입니다. parameter생성자의 본문 내에서 원하는만큼 자주 사용할 수 있으며 항상 같은 객체를 나타냅니다. 암시 적으로 이동하면 위험하므로 언어에서 금지합니다.

명명 된 rvalue 참조는 다른 변수와 마찬가지로 lvalue입니다.

해결책은 수동으로 이동을 활성화하는 것입니다.

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

parameter의 초기화 후 더 이상 사용되지 않는다고 주장 할 수 member있습니다. std::move반환 값과 마찬가지로 자동 삽입하는 특별한 규칙이없는 이유는 무엇 입니까? 아마도 컴파일러 구현 자에게 너무 많은 부담이 있기 때문일 것입니다. 예를 들어 생성자 본문이 다른 번역 단위에 있다면 어떻게해야합니까? 반대로, 반환 값 규칙은 단순히 return키워드 뒤의 식별자 가 자동 개체를 나타내는 지 여부를 결정하기 위해 기호 테이블을 확인하기 만하면 됩니다.

parameterby 값을 전달할 수도 있습니다 . 와 같은 이동 전용 유형의 unique_ptr경우 아직 확립 된 관용구가없는 것 같습니다. 개인적으로, 나는 인터페이스를 덜 어지럽히 기 때문에 값으로 전달하는 것을 선호합니다.

특별 회원 기능

C ++ 98은 필요에 따라 즉, 복사 생성자, 복사 할당 연산자 및 소멸자 등 필요할 때 세 가지 특수 멤버 함수를 암시 적으로 선언합니다.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Rvalue 참조는 여러 버전을 거쳤습니다. 버전 3.0부터 C ++ 11은 필요에 따라 이동 생성자와 이동 할당 연산자라는 두 가지 추가 특수 멤버 함수를 선언합니다. VC10이나 VC11은 아직 버전 3.0을 따르지 않으므로 직접 구현해야합니다.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

이 두 가지 특수 멤버 함수는 특수 멤버 함수를 수동으로 선언하지 않은 경우에만 암시 적으로 선언됩니다. 또한 고유 한 이동 생성자 또는 이동 할당 연산자를 선언하면 복사 생성 자나 복사 할당 연산자가 암시 적으로 선언되지 않습니다.

이 규칙은 실제로 무엇을 의미합니까?

관리되지 않는 리소스가없는 클래스를 작성하는 경우 5 개의 특수 멤버 함수를 직접 선언 할 필요가 없으며 올바른 카피 시맨틱을 가져오고 시맨틱을 무료로 이동할 수 있습니다. 그렇지 않으면 특수 멤버 함수를 직접 구현해야합니다. 물론 클래스가 이동 시맨틱의 이점을 얻지 못하면 특수 이동 작업을 구현할 필요가 없습니다.

복사 대입 연산자와 이동 대입 연산자는 인수를 값으로 사용하여 단일 통합 대입 연산자로 통합 할 수 있습니다.

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

이런 식으로 구현할 특수 멤버 함수의 수는 5에서 4로 감소합니다. 여기에는 예외 안전과 효율성 사이에 상충 관계가 있지만이 문제의 전문가는 아닙니다.

참조를 전달 ( 이전 으로 알려진 유니버설 참조 )

다음 함수 템플릿을 고려하십시오.

template<typename T>
void foo(T&&);

T&&언뜻 보면 rvalue 참조처럼 보이기 때문에 rvalue에만 바인드 할 수 있습니다 . 그러나 밝혀 졌 듯이 T&&lvalue에도 바인딩됩니다.

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

인수 형태의 r- 수치 인 경우 X, T로 도출되고 X, 따라서 T&&의미한다 X&&. 이것은 누구나 기대하는 것입니다. 인수가 유형의 좌변 그러나 만약 X특별한 규칙에 의한는, T로 추론되고 X&, 따라서 T&&같은 것을 의미 할 것입니다 X& &&. 그러나 C ++에는 여전히 참조에 대한 참조 개념이 없으므로 유형 X& &&이로 축소 됩니다 X&. 처음에는 혼란스럽고 쓸모없는 것처럼 들릴 수 있지만 참조 축소는 완벽한 전달에 필수적입니다 (여기에서는 설명하지 않음).

T &&는 rvalue 참조가 아니라 전달 참조입니다. 또한이 경우, lvalues에 결합 T하고 T&&모두 좌변 참조입니다.

함수 템플릿을 rvalue 로 제한하려면 SFINAE 를 유형 특성과 결합 할 수 있습니다 .

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

이사 이행

참조 축소를 이해 했으므로 std::move다음은 구현 방법 입니다.

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

보시다시피 moveforwarding reference 덕분에 모든 종류의 매개 변수를 받아들이고 T&&rvalue 참조를 반환합니다. std::remove_reference<T>::type그렇지 않으면 유형의 lvalues에 대한 때문에 메타 함수 호출이 필요합니다 X, 반환 형식이 될 것 X& &&으로 붕괴 것이다, X&. 때문에 t항상 좌변입니다 (명명를 rvalue 참조가 좌변 있음을 유의)하지만, 우리가 바인딩 할 t를 rvalue 참조에, 우리는 명시 적으로 캐스팅해야 t올바른 리턴 유형으로. rvalue 참조를 리턴하는 함수의 호출 자체는 xvalue입니다. 이제 xvalue의 출처를 알 수 있습니다.)

와 같은 rvalue 참조를 반환하는 함수의 호출은 std::movexvalue입니다.

이 예제에서는 rvalue 참조로 반환하는 t것이 좋습니다. 자동 객체가 아니라 호출자가 전달한 객체를 나타 내기 때문 입니다.



24
이동 의미론이 중요한 세 번째 이유는 예외 안전입니다. 복사 작업이 발생하는 경우 (자원 할당이 필요하고 할당이 실패 할 수 있기 때문에) 이동 작업은 발생하지 않을 수 있습니다 (새로운 작업을 할당하는 대신 기존 자원의 소유권을 이전 할 수 있기 때문에). 실패 할 수없는 연산을 갖는 것이 항상 좋으며 예외 보장을 제공하는 코드를 작성할 때 중요 할 수 있습니다.
Brangdon

8
나는 '유니버설 레퍼런스'까지 당신과 함께 있었지만 따라하기에는 너무 추상적입니다. 참조 축소? 완벽한 전달? 유형이 템플릿 화되면 rvalue 참조가 범용 참조가된다고 말하는가? 이해할 필요가 있는지 알 수 있도록 이것을 설명하는 방법이 있었으면 좋겠습니다. :)
Kylotan

8
지금 책을 쓰십시오 ...이 대답은 C ++의 다른 구석을 이와 같이 명쾌한 방법으로 덮었다면 수천 명의 사람들이 그것을 이해할 것이라고 믿을만한 이유를 제시했습니다.
halivingston

12
@halivingston 친절한 피드백에 감사드립니다. 정말 감사합니다. 책을 쓰는 데있어 문제는 : 상상할 수있는 것보다 훨씬 많은 일입니다. C ++ 11 이상을 깊이 파고 싶다면 Scott Meyers의 "Effective Modern C ++"을 구입하는 것이 좋습니다.
fredoverflow

77

이동 의미론은 rvalue 참조를 기반으로 합니다.
rvalue는 임시 객체이며 표현식의 끝에서 파괴됩니다. 현재 C ++에서 rvalue는 const참조 에만 바인딩됩니다 . C ++ 1x는 rvalue 객체에 대한 const참조 인 철자가 아닌 rvalue 참조 를 허용 T&&합니다.
표현식의 끝에 rvalue가 죽을 것이기 때문에 데이터를 훔칠 수 있습니다 . 다른 객체로 복사 하는 대신 데이터를 객체 로 옮깁니다 .

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

위의 코드에서, 오래된 컴파일러의 결과 f()입니다 복사x사용 X의 복사 생성자를. 컴파일러에서 이동 의미론을 지원 X하고 이동 생성자가 있으면 대신 호출됩니다. 그 이후 rhs인수가이다 를 rvalue , 우리는 더 이상 필요하지 않은 것을 알고 우리는 그 가치를 훔칠 수 있습니다.
따라서 값은 이름이 지정되지 않은 임시에서에서 로 이동f() 합니다 x(의 데이터 x는 empty로 초기화되어 X임시로 이동하여 할당 후에 소멸됩니다).


1
그것이 있어야합니다 this->swap(std::move(rhs));라는 이름의를 rvalue 참조가 lvalues 때문에
wmamrak

이 Tacyt의 코멘트 @ 당, 좀 잘못된 것입니다 : rhs입니다 좌변 의 맥락에서 X::X(X&& rhs). std::move(rhs)rvalue를 얻으려면 전화해야 하지만이 종류의 답변은 무례합니다.
Asherah

포인터가없는 유형의 의미를 이동시키는 것은 무엇입니까? 이동 시맨틱은 좋아하는 복사?
Gusev Slava

@ Gusev : 나는 당신이 무엇을 요구하는지 전혀 모른다.
sbi

60

상당한 객체를 반환하는 함수가 있다고 가정합니다.

Matrix multiply(const Matrix &a, const Matrix &b);

다음과 같은 코드를 작성할 때 :

Matrix r = multiply(a, b);

일반 C ++ 컴파일러는의 결과에 대한 임시 객체를 생성하고 multiply(), 복사 생성자를 호출하여 초기화 r한 다음 임시 반환 값을 삭제합니다. C ++ 0x의 이동 의미는 "이동 생성자"를 호출하여 r내용을 복사 하여 초기화 한 다음,이를 파괴하지 않고 임시 값을 버립니다.

Matrix위 의 예와 같이 복사되는 객체가 힙에 추가 메모리를 할당하여 내부 표현을 저장하는 경우 특히 중요합니다 . 카피 생성자는 내부 표현의 전체 카피를 만들거나 참조 카운팅 및 COW (Copy-On-Write) 의미를 내부적으로 사용해야합니다. 이동 생성자는 힙 메모리 만 남겨두고 포인터를 Matrix객체 내부에 복사 합니다.


2
이동 생성자와 복사 생성자는 어떻게 다릅니 까?
dicroce

1
@ dicroce : 구문에 따라 다릅니다. 하나는 Matrix (const Matrix & src) (복사 생성자)처럼 보이고 다른 하나는 Matrix (Matrix && src) (이동 생성자)처럼 보입니다. 더 나은 예를 보려면 주요 대답을 확인하십시오.
snk_kid

3
@dicroce : 하나는 빈 개체를 만들고 다른 하나는 복사본을 만듭니다. 객체에 저장된 데이터가 크면 사본이 비쌀 수 있습니다. 예를 들어 std :: vector입니다.
Billy ONeal

1
@ kunj2aan : 컴파일러에 달려 있습니다. 컴파일러는 함수 내에 임시 객체를 만든 다음 호출자의 반환 값으로 이동할 수 있습니다. 또는 이동 생성자를 사용하지 않고도 반환 값으로 객체를 직접 구성 할 수 있습니다.
Greg Hewgill

2
@Jichao : 즉 망막 정맥 폐쇄라는 최적화, 차이에 대한 자세한 내용은이 질문을 볼 수 있습니다 : stackoverflow.com/questions/5031778/...
그렉 Hewgill

30

이동 의미론에 대한 심도 있고 깊이있는 설명에 관심이 있다면 "C ++ 언어에 동작 의미 론적 지원을 추가하기위한 제안" 에 원본 논문을 읽는 것이 좋습니다 .

접근하기 쉽고 읽기 쉬우 며 제공하는 이점에 대한 훌륭한 사례가됩니다. WG21 웹 사이트 에서 사용할 수있는 이동 의미론에 대한 최신의 최신 문서가 있지만이 문서는 최상위 수준의 관점에서 접근하기 때문에 가장 간단한 문서 일 것입니다.


27

이동 의미론 은 더 이상 소스 값이 필요하지 않을 때 자원을 복사하는 것이 아니라 자원을 전송하는 것 입니다.

C ++ 03에서는 객체가 종종 복사되며 코드에서 값을 다시 사용하기 전에 소멸되거나 할당됩니다. 예를 들어, RVO가 시작되지 않는 한 함수에서 값으로 반환하면 반환하는 값이 호출자의 스택 프레임에 복사 된 다음 범위를 벗어나 파괴됩니다. 이것은 많은 예 중 하나 일뿐입니다. 소스 객체가 일시적인 경우 sort값별 전달, 항목을 다시 정렬하는 것과 같은 알고리즘 , 초과 된 vector경우의 재 할당 capacity()등을 참조하십시오.

이러한 복사 / 파괴 쌍이 비싸면 일반적으로 개체에 무거운 리소스가 있기 때문입니다. 예를 들어, 각각 고유의 동적 메모리를 가진 객체 vector<string>배열을 포함하는 동적으로 할당 된 메모리 블록을 소유 할 수 있습니다 string. 이러한 객체를 복사하는 것은 비용이 많이 듭니다. 소스에서 동적으로 할당 된 각 블록에 새 메모리를 할당하고 모든 값을 복사해야합니다. 그런 다음 방금 복사 한 모든 메모리를 할당 해제해야합니다. 그러나 크게 이동vector<string> 한다는 것은 몇 개의 포인터 (동적 메모리 블록 참조)를 대상에 복사하고 소스에서 제로화하는 것입니다.


23

쉬운 (실용적인) 용어로 :

객체를 복사한다는 것은 "정적"멤버를 복사 new하고 동적 객체를 위해 연산자를 호출하는 것을 의미 합니다. 권리?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

그러나 객체 를 이동 한다는 것은 (실제적인 관점에서 반복합니다) 동적 객체의 포인터를 복사하는 것만 의미하며 새로운 객체는 만들지 않습니다.

그러나 위험하지 않습니까? 물론 동적 객체를 두 번 파괴 할 수 있습니다 (세그먼트 결함). 따라서이를 피하려면 소스 포인터를 "무효화"하여 두 번 파괴하지 않도록해야합니다.

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

좋아, 그러나 객체를 움직이면 소스 객체가 쓸모 없어집니다. 물론 특정 상황에서는 매우 유용합니다. 가장 확실한 것은 익명의 객체 (임시, rvalue 객체, ..., 다른 이름으로 호출 할 수 있음)로 함수를 호출 할 때입니다.

void heavyFunction(HeavyType());

이 경우 익명 오브젝트가 작성되고 다음에 함수 매개 변수에 복사 된 후 삭제됩니다. 따라서 익명 개체가 필요하지 않고 시간과 메모리를 절약 할 수 있으므로 개체를 이동하는 것이 좋습니다.

이는 "rvalue"참조의 개념으로 이어집니다. 수신 된 객체가 익명인지 아닌지를 감지하기 위해서만 C ++ 11에 존재합니다. "lvalue"가 할당 가능한 엔터티 ( =연산자 의 왼쪽 부분)라는 것을 이미 알고 있으므로 lvalue로 작동하려면 개체에 대한 명명 된 참조가 필요합니다. rvalue는 명명 된 참조가없는 객체와 정확히 반대입니다. 따라서 익명 객체와 rvalue는 동의어입니다. 그래서:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

이 경우, 유형의 객체를 A"복사"해야하는 경우 컴파일러는 전달 된 객체의 이름이 지정되었는지 여부에 따라 lvalue 참조 또는 rvalue 참조를 작성합니다. 그렇지 않으면 이동 생성자가 호출되고 객체가 시간적이라는 것을 알고 동적 객체를 복사하는 대신 이동하여 공간과 메모리를 절약 할 수 있습니다.

"정적"객체는 항상 복사된다는 것을 기억해야합니다. 정적 객체 (스택이 아닌 스택의 객체)를 "이동"할 수있는 방법이 없습니다. 따라서 개체에 (직접 또는 간접적으로) 동적 멤버가없는 경우 "이동"/ "복사"구별은 중요하지 않습니다.

객체가 복잡하고 소멸자가 라이브러리 함수 호출, 다른 전역 함수 호출 또는 그 밖의 다른 함수와 같은 다른 보조 효과가있는 경우 플래그로 움직임을 알리는 것이 좋습니다.

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

따라서 코드가 짧아지고 ( nullptr동적 멤버별로 할당 할 필요가 없음 ) 더 일반적입니다.

다른 일반적인 질문 : A&&과 의 차이점은 무엇 const A&&입니까? 물론 첫 번째 경우에는 객체를 수정할 수 있고 두 번째 경우에는 실용적이지 않습니까? 두 번째 경우에는 객체를 수정할 수 없으므로 객체를 무효화하는 방법이 없으며 (변경 가능한 플래그 또는 이와 유사한 것을 제외하고) 복사 생성자와 실질적인 차이가 없습니다.

그리고 완벽한 전달 이란 무엇 입니까? "rvalue reference"는 "caller 's scope"에서 명명 된 객체에 대한 참조라는 것을 아는 것이 중요합니다. 그러나 실제 범위에서 rvalue 참조는 객체의 이름이므로 명명 된 객체로 작동합니다. rvalue 참조를 다른 함수에 전달하면 명명 된 객체가 전달되므로 객체는 임시 객체처럼 수신되지 않습니다.

void some_function(A&& a)
{
   other_function(a);
}

객체 a는의 실제 매개 변수에 복사됩니다 other_function. 객체가 a계속 임시 객체로 취급되도록하려면 std::move함수를 사용해야합니다 .

other_function(std::move(a));

이 줄 을 사용하면 rvalue로 std::move캐스트 a되고 other_function객체를 이름이없는 객체로받습니다. 물론 other_function명명되지 않은 객체로 작업하기 위해 특정 오버로드가없는 경우이 구분은 중요하지 않습니다.

완벽한 전달인가요? 아닙니다. 그러나 우리는 매우 가깝습니다. 퍼펙트 포워딩은 다음과 같은 목적으로 템플릿 작업에만 유용합니다. 객체를 다른 함수에 전달해야하는 경우 명명 된 객체를 수신하면 객체가 명명 된 객체로 전달되고 그렇지 않은 경우에는 명명되지 않은 객체처럼 전달하고 싶습니다.

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

이것이 C ++ 11에서 구현 된 완벽한 전달을 사용하는 프로토 타입 함수의 시그니처입니다 std::forward. 이 함수는 템플릿 인스턴스화 규칙을 활용합니다.

 `A& && == A&`
 `A&& && == A&&`

따라서, 경우 T에 좌변 기준이다 A( T = A &), a또한 ( A & && => A &가). 이 경우 T에 참조 r- 수치이고 A, a또한 (A && && =>를 &&). 두 경우 모두 a실제 범위의 명명 된 개체이지만 T호출자 범위의 관점에서 "참조 유형"정보를 포함합니다. 이 정보 ( T)는 템플릿 매개 변수로 전달되며 forward의 유형에 따라 'a'가 이동되거나 이동되지 않습니다 T.


20

복사 의미론과 비슷하지만 모든 데이터를 복제하는 대신 "이동"할 개체에서 데이터를 훔쳐 야합니다.


13

카피 의미가 무엇을 의미하는지 알고 있습니까? 복사 가능한 유형이 있음을 의미합니다. 사용자 정의 유형의 경우 복사 생성자 및 할당 연산자를 명시 적으로 작성하거나 컴파일러가 암시 적으로 생성합니다. 이것은 사본을 수행합니다.

이동 의미론은 기본적으로 r- 값 참조 (&& (예 : 두 앰퍼샌드를 사용하는 새로운 참조 유형)를 사용하는 생성자가있는 사용자 정의 유형이며, 이는 상수가 아니며 이동 생성자라고하며 할당 연산자와 동일합니다. 따라서 이동 생성자는 메모리를 소스 인수에서 복사하는 대신 소스에서 대상으로 메모리를 '이동'합니다.

언제하고 싶니? 잘 std :: vector는 예입니다. 임시 std :: vector를 작성하고 함수 say에서 반환한다고 가정하십시오.

std::vector<foo> get_foos();

std :: vector가 복사하는 대신 이동 생성자가 있으면 포인터를 설정하고 동적으로 할당 할 수 있습니다. 새 인스턴스에 대한 메모리. std :: auto_ptr을 사용하는 소유권 이전 의미론과 비슷합니다.


1
이 함수 반환 값 예제에서 반환 값 최적화가 이미 복사 작업을 제거하고 있기 때문에 이것이 좋은 예라고 생각하지 않습니다.
Zan Lynx

7

이동 의미론 의 필요성을 설명하기 위해 이동 의미론 없이이 예제를 고려해 보겠습니다.

다음은 유형의 객체를 가져와 T같은 유형의 객체를 반환하는 함수입니다 T.

T f(T o) { return o; }
  //^^^ new object constructed

위의 함수는 값으로 호출을 사용 합니다. 즉,이 함수가 호출 될 때 함수가 사용하도록 객체를 구성 해야합니다 .
이 함수는 value 로도 리턴 하므로 리턴 값에 대해 또 다른 새 오브젝트가 구성됩니다.

T b = f(a);
  //^ new object constructed

두 개의 새로운 객체가 만들어졌으며 그 중 하나는 기능 기간 동안 만 사용되는 임시 객체입니다.

반환 값 에서 새 개체를 만들면 임시 개체의 내용을 새 개체 로 복사 하기 위해 복사 생성자가 호출됩니다 . b. 함수가 완료된 후 함수에 사용 된 임시 오브젝트가 범위를 벗어나서 소멸됩니다.


이제 복사 생성자 가 하는 일을 생각해 봅시다 .

먼저 객체를 초기화 한 다음 모든 관련 데이터를 이전 객체에서 새 객체로 복사해야합니다.
클래스에 따라 데이터가 많은 컨테이너 일 수 있으며 많은 시간메모리 사용을 나타낼 수 있습니다.

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

이동 의미론을 사용하면 복사가 아닌 단순히 데이터 를 이동 하여 대부분의 작업을 덜 불쾌하게 만들 수 있습니다.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

데이터를 이동하려면 데이터를 새 개체와 다시 연결해야합니다. 그리고 전혀 사본이 없습니다 .

이것은 rvalue참조 로 달성됩니다 . 참조 거의처럼 작동 한 가지 중요한 차이 참조 : 를 rvalue 참조가 이동 될 수좌변이 없습니다.
rvaluelvalue

에서 cppreference.com :

강력한 예외 보장을 가능하게하기 위해 사용자 정의 이동 생성자는 예외를 발생시키지 않아야합니다. 실제로 표준 컨테이너는 일반적으로 std :: move_if_noexcept를 사용하여 컨테이너 요소를 재배치해야 할 때 이동 및 복사 중에서 선택합니다. 복사 및 이동 생성자가 모두 제공되는 경우 인수가 rvalue (이름없는 임시와 같은 prvalue 또는 std :: move의 결과와 같은 xvalue) 인 경우 과부하 해결은 이동 생성자를 선택하고, 복사 생성자를 선택합니다. 인수는 lvalue (명명 된 오브젝트 또는 lvalue 참조를 리턴하는 함수 / 연산자)입니다. 복사 생성자 만 제공되는 경우 모든 인수 범주는 rvalue가 const 참조에 바인딩 될 수 있으므로 const에 대한 참조가 필요한 한 해당 인수를 선택합니다. 많은 상황에서 이동 생성자는 관찰 가능한 부작용을 생성하더라도 최적화됩니다. 복사 제거를 참조하십시오. rvalue 참조를 매개 변수로 사용하는 생성자를 '이동 생성자'라고합니다. 아무것도 이동해야 할 의무는 없으며 클래스는 이동할 리소스가 없어도되며 'move constructor'은 매개 변수가 다음과 같은 허용 가능한 (그러나 합당하지 않은) 경우처럼 리소스를 이동할 수 없습니다. const rvalue 참조 (const T &&).


7

나는 이것을 올바르게 이해하기 위해 이것을 쓰고 있습니다.

대형 객체의 불필요한 복사를 피하기 위해 이동 의미론이 작성되었습니다. Bjarne Stroustrup은 자신의 저서 "C ++ Programming Language"에서 기본적으로 불필요한 복사가 발생하는 두 가지 예를 사용합니다. 하나는 두 개의 큰 객체를 교체하고 두 번째는 메소드에서 큰 객체를 반환합니다.

두 개의 큰 객체를 서로 바꾸려면 일반적으로 첫 번째 객체를 임시 객체로 복사하고, 두 번째 객체를 첫 번째 객체로 복사하고, 임시 객체를 두 번째 객체로 복사합니다. 내장 유형의 경우 매우 빠르지 만 큰 객체의 경우이 세 복사본에 많은 시간이 걸릴 수 있습니다. "이동 할당"을 사용하면 프로그래머가 기본 복사 동작을 무시하고 대신 객체에 대한 참조를 교체 할 수 있습니다. 즉, 복사가 전혀없고 교체 작업이 훨씬 빠릅니다. std :: move () 메소드를 호출하여 이동 지정을 호출 할 수 있습니다.

기본적으로 메서드에서 개체를 반환하려면 호출자가 액세스 할 수있는 위치에 로컬 개체와 관련 데이터를 복사해야합니다 (로컬 개체는 호출자가 액세스 할 수없고 메서드가 완료되면 사라지기 때문에). 내장 유형이 리턴 될 때이 조작은 매우 빠르지 만 큰 오브젝트가 리턴되는 경우 시간이 오래 걸릴 수 있습니다. 이동 생성자는 프로그래머가이 기본 동작을 무시하고 대신 호출자에게 리턴되는 오브젝트가 로컬 오브젝트와 연관된 힙 데이터를 가리 키도록하여 로컬 오브젝트와 연관된 힙 데이터를 "재사용"할 수 있도록합니다. 따라서 복사가 필요하지 않습니다.

로컬 객체 (스택의 객체)를 만들 수없는 언어에서는 모든 객체가 힙에 할당되고 항상 참조로 액세스되므로 이러한 유형의 문제는 발생하지 않습니다.


""할당 이동 "을 사용하면 프로그래머가 기본 복사 동작을 무시하고 대신 객체에 대한 참조를 교체 할 수 있습니다. 즉, 복사가 전혀없고 교체 작업이 훨씬 빠릅니다." -이러한 주장은 모호하고 오해의 소지가 있습니다. 두 개체를 교환 x하고 y, 할 수 있습니다뿐만 아니라 "객체에 스왑 참조" ; 객체에 다른 데이터를 참조하는 포인터가 포함되어있을 수 있으며 해당 포인터를 교체 할 수 있지만 이동 연산자는 아무것도 교체 하지 않아도 됩니다. 대상 데이터를 보존하지 않고 이동 한 객체에서 데이터를 지울 수 있습니다.
Tony Delroy

swap()이동 의미론없이 작성할 수 있습니다. "std :: move () 메소드를 호출하여 이동 지정을 호출 할 수 있습니다." - 그건 가끔 사용할 필요 std::move()가 실제로 아무것도 이동하지 않습니다하지만 - - 그냥 컴파일러는 인수가 때때로 움직일 알 수 있습니다 std::forward<>()(참조를 전달 포함) 및 기타 시간 컴파일러는 값이 이동할 수 있습니다 알고있다.
Tony Delroy

-2

다음 은 Bjarne Stroustrup의 "The C ++ Programming Language"책에 대한 답변 입니다. 비디오를보고 싶지 않다면 아래 텍스트를 볼 수 있습니다.

이 스 니펫을 고려하십시오. 교환 원 +에서 복귀하려면 결과를 로컬 변수 res에서 호출자가 액세스 할 수있는 위치로 복사해야 합니다.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

우리는 실제로 사본을 원하지 않았습니다. 우리는 함수에서 결과를 얻고 싶었습니다. 따라서 Vector를 복사하지 않고 이동해야합니다. 이동 생성자를 다음과 같이 정의 할 수 있습니다.

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&&는 "rvalue reference"를 의미하며 rvalue를 바인딩 할 수있는 참조입니다. "rvalue" '는 "lvalue"를 보완하기위한 것으로 "대략의 왼쪽에 나타날 수있는 것"을 의미합니다. 따라서 rvalue는 함수 호출에 의해 반환되는 정수 및 resVectors에 대한 operator + () 의 로컬 변수 와 같이 대략 "할당 할 수없는 값"을 의미합니다 .

이제 성명서 return res;는 복사되지 않습니다!

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