c++ - 시맨틱 - 이동 의미 란 무엇입니까?




move constructor (8)

C++0x 와 관련된 Scott Meyers와의 Software Engineering 라디오 podcast 인터뷰 를 방금 C++0x . 새로운 기능의 대부분은 나에게 의미가 있었고 실제로는 C ++ 0x에 대해 흥분했습니다. 나는 여전히 움직이는 의미를 얻지 못한다 ... 정확히 무엇인가?


나의 첫 번째 대답은 의미 이동에 대한 매우 단순화 된 소개 였고 많은 세부 사항은 간단하게 유지하기 위해 의도적으로 생략되었습니다. 그러나 의미를 옮기는 데는 훨씬 더 많은 것이 있습니다. 그리고 나는 그 격차를 메꾸기위한 두 번째 해답이 필요할 때라고 생각했습니다. 첫 번째 대답은 이미 꽤 오래된 것이고 완전히 다른 텍스트로 바꾸는 것이 옳다고 생각하지 않습니다. 나는 아직도 첫 번째 소개로 잘 작동한다고 생각합니다. 그러나 더 깊게 파고 들길 원한다면 다음을 읽으십시오 :)

Stephan T. Lavavej는 귀중한 피드백을 제공했습니다. 고마워요, 스테판!

소개

Move semantics는 특정 조건 하에서 객체가 다른 객체의 외부 리소스에 대한 소유권을 가질 수있게합니다. 이것은 두 가지면에서 중요합니다.

  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 ++ 11에서 std::unique_ptr 로 대체 된 사용되지 않는 C ++ 98 표준 라이브러리 템플릿에 대해 설명합니다. 중급 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 | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

ba b 의 초기화가 삼각형을 복사하지 않고 대신 삼각형의 소유권을 a 에서 b 로 옮기는 방법에 유의하십시오. 우리는 또한 " ab 로 이동 "또는 "삼각형이 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 을 사용하지 않도록주의해야합니다.

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();

그러나 아직 그들 중 하나는 정의되지 않은 행동을 불러 일으키지 만 다른 행동은 그렇지 않습니다. 그렇다면 표현식 amake_triangle() 의 차이점은 무엇입니까? 둘 다 같은 유형이 아닌가? 사실 그들은 그렇지만 서로 다른 가치 범주 를 가지고 있습니다.

값 카테고리

분명히, auto_ptr 변수를 나타내는 표현식 aauto_ptr 을 값으로 반환하는 함수 호출을 나타내는 make_triangle() 표현식 사이에 상당한 차이가 있어야합니다. 따라서 호출 될 때마다 새로운 임시 auto_ptr 객체가 생성됩니다 . alvalue 의 예이고 make_triangle()rvalue 의 예입니다.

a 와 같은 lvalues에서의 이동은 위험하다. 나중에 a를 통해 멤버 함수를 호출하여 정의되지 않은 동작을 호출 할 수 있기 때문이다. 반면에 make_triangle() 과 같은 rvalues에서의 이동은 복사 생성자가 작업을 완료 한 후에 다시 임시로 사용할 수 없으므로 완벽하게 안전합니다. 일시적으로 나타내는 표현이 없습니다. make_triangle() 다시 작성하면 다른 임시 정보를 얻게됩니다. 실제로 이동 된 임시는 이미 다음 줄로 넘어갔습니다.

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

문자 lr 은 과제의 왼쪽과 오른쪽에 역사적인 기원을 가지고 있습니다. 이것은 더 이상 C ++에서 true가 아닙니다. 대입 연산자의 왼쪽에 나타낼 수없는 lvalue (대입 연산자가없는 배열이나 사용자 정의 유형과 같음)가있을 수 있고, rvalues가 있습니다 (클래스 유형의 모든 직사각형 대입 연산자 사용).

클래스 유형의 rvalue는 평가로 인해 임시 객체를 만드는 표현식입니다. 정상적인 상황에서 동일한 범위 내의 다른 표현식은 동일한 임시 객체를 나타냅니다.

평가 절 참조

이제 lvalues에서의 이동은 위험 할 수 있지만 rvalues에서의 이동은 무해하다는 것을 이해합니다. C ++에 lvalue 인수와 rvalue 인수를 구별하기위한 언어 지원이 있다면, 우리는 lvalues로부터의 이동을 완전히 금지하거나 적어도 call site에 명시된 lvalues로부터 이동하여 우연히 더 이상 이동하지 않게 할 수 있습니다.

이 문제에 대한 C ++ 11의 답은 rvalue references 입니다. rvalue 참조는 rvalues에만 바인딩되는 새로운 종류의 참조이며 구문은 X&& 입니다. 오래된 참조 X& 는 현재 좌변 값 참조라고 합니다. ( X&& 는 참조에 대한 참조가 아니므로 C ++에는 그러한 것이 없습니다).

mix에 const 를 던지면 이미 네 가지 다른 종류의 참조가 있습니다. 유형 X 의 표현은 어떤 종류의 바인딩 일 수 있습니까?

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

실제로 const X&& 는 잊어 버릴 수 있습니다. rvalues에서 읽기로 제한되는 것은별로 유용하지 않습니다.

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

암시 적 전환

우회 참조는 여러 버전을 거쳤습니다. 버전 2.1부터 rvalue reference X&& 는 다른 유형 Y 의 모든 값 범주에도 바인딩됩니다. 단, Y 에서 X 암시 적으로 변환되어야합니다. 이 경우 임시 X 유형이 만들어지고 rvalue 참조는 해당 임시에 바인딩됩니다.

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

some_function("hello world");

위의 예에서 "hello world"const char[12] 유형의 lvalue입니다. const char[12] 에서 const char* 를 거쳐 std::string 으로 암시 적으로 변환되기 때문에 임시 std::string 형식이 만들어지고 r 은 해당 임시 r 에 바인딩됩니다. 이는 rvalues ​​(표현식)와 temporaries (객체)의 구분이 약간 흐릿한 경우 중 하나입니다.

생성자 이동

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

C ++ 11에서는 std::auto_ptr<T> 가 rvalue 참조를 이용하는 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;
    }

이제 재미있는 부분 인 move 생성자가 나온다.

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

이 이동 생성자는 auto_ptr 복사 생성자가 수행 한 작업을 정확하게 수행하지만 rvalues ​​만 제공 할 수 있습니다.

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

두 번째 줄은 lvalue이기 때문에 컴파일에 실패하지만 unique_ptr&& source 매개 변수는 rvalues에만 바인딩 할 수 있습니다. 이것은 정확히 우리가 원했던 것입니다. 위험한 움직임은 절대로 암시되어서는 안됩니다. 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;
    }
};

sourceunique_ptr 유형의 변수이므로 이동 생성자에 의해 초기화됩니다. 즉, 인수가 매개 변수로 이동됩니다. 이동 생성자 자체에 rvalue 참조 매개 변수가 있기 때문에 인수는 여전히 rvalue가되어야합니다. 제어 흐름이 operator= 의 닫기 중괄호에 도달하면 source 는 범위를 벗어나 이전 자원을 자동으로 해제합니다.

이동 할당 연산자는 관리되는 리소스의 소유권을 현재 개체로 이전하여 이전 리소스를 릴리스합니다. 이동 및 스왑 이디엄은 구현을 단순화합니다.

lvalues에서 이동

때때로, 우리는 좌변가에서 벗어나기를 원합니다. 즉, 컴파일러가 lvalue를 rvalue 인 것처럼 취급하여 잠재적으로 안전하지 않을 수도 있지만 이동 생성자를 호출 할 수있게하려는 경우가 있습니다. 이를 위해 C ++ 11은 <utility> 헤더 내에 표준 라이브러리 함수 템플릿 인 std::move 를 제공합니다. std::move 는 lvalue를 단순히 rvalue std::move 하기 때문에이 이름은 약간 불행합니다. 그것은 아무 것도 움직이지 않는다. 그것은 단지 움직일 수있게 해줍니다 . 어쩌면 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)명시 적으로 작성했기 때문에 우리는 의도를 분명히했습니다. "친애하는 생성자입니다. c 를 초기화하기 위해 원하는 모든 작업을 수행하고 a 더 이상 걱정하지 않습니다. 너의 길은. "

std::move(some_lvalue) 는 lvalue를 rvalue로 변환하여 이후 이동을 가능하게합니다.

X 값

std::move(a) 가 rvalue 임에도 불구하고 평가는 임시 객체를 만들지 않습니다 . 이러한 수수께끼로 인해위원회는 세 번째 가치 범주를 도입해야했습니다. rvalue 참조에 바인딩 될 수있는 것은 비록 전통적인 의미에서 rvalue가 아니더라도 xvalue (eXpiring value)라고합니다. 전통적인 rvalues는 prvalues (Pure rvalues)로 변경되었습니다.

prvalues와 xvalues는 모두 rvalues입니다. X 값과 L 값은 모두 glvalues입니다 (일반화 된 값). 관계는 다이어그램으로 이해하기 쉽습니다.

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

xvalue만이 정말로 새로운 것입니다. 나머지는 이름 바꾸기와 그룹화에 의한 것입니다.

C ++ 98의 rvalues는 C ++ 11에서 prvalues로 알려져 있습니다. 정신적으로 이전 단락의 모든 "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 를 작성할 필요없이 자동 객체를 함수에서 반환 할 수있는 특별한 규칙이 있습니다. 사실, 함수를 사용하여 자동 객체를 이동 시키지 않으면 "named return value optimization"(NRVO)을 사용 하지 않으므로 std::move 를 사용하면 안됩니다.

자동 객체를 함수 밖으로 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 참조에 바인딩하는 것이 아닙니다.

회원으로 이동

조만간, 여러분은 다음과 같은 코드를 작성할 것입니다.

class Foo
{
    unique_ptr<Shape> member;

public:

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

기본적으로 컴파일러는 parameter 가 lvalue임을 불평 할 것입니다. 유형을 살펴보면 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
    {}
};

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

값을 기준으로 parameter 를 전달할 수도 있습니다. unique_ptr 과 같은 이동 전용 유형의 경우 아직 설정된 관용구가없는 것 같습니다. 개인적으로, 필자는 값으로 전달하는 것을 선호합니다. 인터페이스에서 덜 혼란 스럽기 때문입니다.

특별 회원 기능

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

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

우회 참조는 여러 버전을 거쳤습니다. 버전 3.0 이후로 C ++ 11은 필요할 때마다 두 개의 추가 멤버 함수, 이동 생성자 및 이동 할당 연산자를 선언합니다. VC10도 VC11도 버전 3.0을 따르지 않으므로 직접 구현해야합니다.

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

이 두 개의 새로운 특수 멤버 함수는 수동으로 선언 된 특별한 멤버 함수가 없으면 암시 적으로 선언됩니다. 또한 자신의 이동 생성자 또는 할당 연산자를 선언하면 복사 생성자 나 복사 할당 연산자가 암시 적으로 선언되지 않습니다.

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

관리되지 않는 리소스가없는 클래스를 작성하는 경우 다섯 가지 특수 멤버 함수 중 하나를 직접 선언 할 필요가 없으며 올바른 복사 의미 및 이동 의미를 무료로 얻을 수 있습니다. 그렇지 않으면 특별한 멤버 함수를 직접 구현해야합니다. 물론 클래스가 이동 의미론의 이점을 얻지 못하면 특수 이동 작업을 구현할 필요가 없습니다.

복사 할당 연산자와 이동 대입 연산자는 하나의 통합 대입 연산자로 인수를 인수로 취할 수 있습니다.

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

이렇게하면 구현할 특수 멤버 함수의 수가 5에서 4로 떨어집니다. 여기에는 예외 안전성과 효율성 사이의 상충 관계가 있지만이 문제에 대한 전문가는 아닙니다.

전달 참조 ( previously 에는 Universal 참조 라고 함)

다음 함수 템플리트를 고려하십시오.

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

T&& 가 rvalue에만 바인딩 할 것으로 예상 할 수 있습니다. 이는 언뜻보기에는 r 값 참조와 비슷하기 때문입니다. T&& 는 lvalues에도 바인딩됩니다.

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>&

인수가 X 유형의 rvalue 인 경우, TX 로 추론됩니다. 따라서 T&&X&& 의미합니다. 이것은 누구나 기대할 수있는 것입니다. 그러나 인수가 X 형의 lvalue이고 특별한 규칙으로 인해 TX& 인 것으로 추론되므로 T&&X& && 와 같은 것을 의미합니다. 그러나 C ++에는 여전히 참조에 대한 참조가 없으므로 X& && 유형은 X& &&축소 됩니다. 이것은 혼란스럽고 처음에는 쓸모가 없을 지 모르지만, 완벽한 포워딩을 위해서는 참조 축소가 필수적입니다 (여기서는 설명하지 않겠습니다).

T &&는 r 값 참조가 아니라 전달 참조입니다. 또한 Lvalues에 바인딩됩니다.이 경우 TT&& 는 모두 왼쪽 값 참조입니다.

함수 템플릿을 rvalues로 제한하려면 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);
}

보시다시피, move 는 전달 참조 T&& 덕분에 모든 종류의 매개 변수를 허용하고 rvalue 참조를 반환합니다. 그렇지 않으면, 유형 X Lvalue의 경우 리턴 유형이 X& && 가되어 X& .로 축소 될 것이므로 std::remove_reference<T>::type 메타 기능 호출이 필요합니다. t 는 항상 lvalue이므로 (명명 된 rvalue 참조가 lvalue 임), t 를 rvalue 참조에 바인딩하려면 t 를 올바른 반환 유형으로 명시 적으로 변환해야합니다. rvalue 참조를 반환하는 함수의 호출 자체가 xvalue입니다. 이제 xvalue가 어디에서 왔는지 알 수 있습니다.)

rvalue 참조 (예 : std::move 를 반환하는 함수의 호출은 x 값입니다.

이 예제에서는 t 참조로 반환하는 것이 좋지만 t 는 자동 개체를 나타내지 않고 대신 호출자가 전달한 개체를 나타 내기 때문에주의하십시오.


실질적인 객체를 반환하는 함수가 있다고 가정 해보십시오.

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

이런 코드를 작성할 때 :

Matrix r = multiply(a, b);

보통의 C ++ 컴파일러는 multiply() 의 결과를위한 임시 객체를 만들고 복사 생성자를 호출하여 r 을 초기화 한 다음 임시 반환 값을 소멸시킵니다. C ++ 0x에서 시맨틱을 움직이면 "move constructor"을 호출하여 내용을 복사하여 r 을 초기화 한 다음 임시 값을 파기하지 않고 폐기합니다.

위의 Matrix 예제와 같이 복사되는 객체가 힙에 내부 메모리를 저장하기 위해 여분의 메모리를 할당하는 경우 특히 중요합니다. 복사 생성자는 내부 표현의 전체 복사본을 만들거나 참조 카운팅 및 copy-on-write 의미를 중간에 사용해야합니다. 이동 생성자는 힙 메모리 만 남기고 Matrix 객체 내부에 포인터를 복사합니다.


이동 의미는 rvalue 참조를 기반으로합니다.
rvalue는 임시 객체로 표현식이 끝날 때 파괴됩니다. 현재 C ++에서 rvalues는 const 참조에만 바인딩됩니다. C ++ 1x는 rvalue 오브젝트에 대한 참조 인 T&& 가 철자가 아닌 비 rvalue 참조를 허용합니다.
표현의 끝에서 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 (빈 X 초기화 된 x 의 데이터는 임시로 이동되며 할당 후 소멸됩니다)로 이동합니다.


이동 의미 는 아무도 소스 값을 더 이상 필요로하지 않을 때 복사하는 것이 아니라 리소스전송하는 것 입니다.

C ++ 03에서는 어떤 코드가 값을 다시 사용하기 전에 객체가 복사되거나 파괴되거나 할당되기도합니다. 예를 들어 함수에서 값을 반환하면 RVO가 실행되지 않으면 반환하는 값이 호출자의 스택 프레임으로 복사 된 다음 범위를 벗어나 파괴됩니다. 이것은 많은 예제 중 하나 일뿐입니다. 소스 객체가 임시 일 때 값 별 전달, sort아이템 재 배열, 초과 vector시점의 재 할당 등의 알고리즘을 참조하십시오 capacity().

그러한 복사 / 삭제 쌍이 비용이 많이 드는 경우 일반적으로 개체가 중량이 많은 리소스를 소유하고 있기 때문입니다. 예를 들어, 객체 vector<string>의 배열을 포함하는 동적으로 할당 된 메모리 블록을 소유 할 수 있습니다. string각 메모리 블록 에는 자체 동적 메모리가 있습니다. 이러한 객체를 복사하는 것은 비용이 많이 든다. 소스의 동적으로 할당 된 각 블록에 대해 새 메모리를 할당하고 모든 값을 복사해야한다. 그런 다음 복사 한 메모리를 모두 할당 해제해야합니다. 그러나 대용량을 이동vector<string> 한다는 것은 동적 메모리 블록을 참조하는 몇 개의 포인터를 대상에 복사하고 원본에서 제로화하는 것입니다.


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

이동 시맨틱은 대형 오브젝트의 불필요한 복사를 피하기 위해 작성되었습니다. Bjarne Stroustrup은 "C ++ 프로그래밍 언어"에서 불필요한 복사가 기본적으로 발생하는 두 가지 예제를 사용합니다. 하나는 두 개의 큰 객체를 교환하고 두 개는 메소드에서 대형 객체를 반환하는 것입니다.

두 개의 대형 오브젝트를 교환하는 일은 보통 첫 번째 오브젝트를 임시 오브젝트에 복사하고 두 번째 오브젝트를 첫 번째 오브젝트에 복사 한 다음 임시 오브젝트를 두 번째 오브젝트에 복사하는 작업을 포함합니다. 내장 유형의 경우 매우 빠르지 만 대형 객체의 경우이 세 개의 사본에 많은 시간이 걸릴 수 있습니다. "이동 할당"을 사용하면 프로그래머가 기본 복사 동작을 대체하고 대신 객체에 대한 참조를 바꿀 수 있습니다. 따라서 복사가 전혀없고 스왑 작업이 훨씬 빠릅니다. 이동 할당은 std :: move () 메서드를 호출하여 호출 할 수 있습니다.

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

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


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

객체 복사는 "정적"멤버를 복사 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. 
};

좋아요,하지만 객체를 움직이면 소스 객체는 쓸모 없게됩니다. 물론, 특정 상황에서는 매우 유용합니다. 가장 명백한 것은 익명의 객체 (시간적, 우발적 객체, ..., 다른 이름으로 호출 할 수있는)로 함수를 호출 할 때입니다.

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"복사"해야 할 때 컴파일러는 전달 된 객체의 이름이 지정되었는지 여부에 따라 왼쪽 값 참조 또는 오른쪽 값 참조를 만듭니다. 그렇지 않으면 이동 생성자가 호출되며 객체가 일시적임을 알게되고 동적 객체를 복사하는 대신 공간 및 메모리를 절약 할 수 있습니다.

"정적"객체는 항상 복사된다는 것을 기억하는 것이 중요합니다. 정적 객체를 "이동"할 수있는 방법은 없습니다 (스택의 객체이며 힙이 아닙니다). 따라서 객체에 동적 멤버가 없으면 (직접 또는 간접적으로) 객체가 이동할 때 "이동"/ "복사"라는 구별은 부적합합니다.

객체가 복잡하고 소멸자가 라이브러리 함수 호출, 다른 전역 함수 호출 또는 기타 무엇이든간에 다른 보조 효과를 갖는 경우 플래그가있는 움직임을 신호하는 것이 좋습니다.

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"는 "호출자의 범위"에있는 명명 된 객체에 대한 참조임을 알아야합니다. 그러나 실제 범위에서 rvalue 참조는 객체의 이름이므로 명명 된 객체의 역할을합니다. rvalue 참조를 다른 함수에 전달하면 명명 된 객체를 전달하므로 객체가 임시 객체처럼 수신되지 않습니다.

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

객체 a는의 실제 매개 변수로 복사됩니다 other_function. 객체를 a임시 객체로 계속 처리하려면 다음 std::move함수를 사용해야합니다 .

other_function(std::move(a));

이 선 을 사용하여 rvalue로 std::move캐스팅 하고 객체를 이름없는 객체로 수신합니다. 물론, 명명되지 않은 객체를 사용하여 작업 할 특정 오버로드가 없다면 이 구분은 중요하지 않습니다.aother_functionother_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 &). 만약 에 r 값 이 있다면 , 또한 (A && && => A &&). 두 경우 모두 실제 범위의 명명 된 객체이지만 호출자 범위의 관점에서 볼 때 "참조 유형"정보를 포함합니다. 이 정보 ( )는 템플릿 매개 변수로 전달되며 'a'는 유형에 따라 이동되거나 이동하지 않습니다 .AaTAaaTTforwardT


복사 의미가 옳다는 것을 알고 있습니까? 즉, 복사 할 수있는 유형이 있음을 의미합니다. 사용자 정의 유형의 경우 복사 생성자 및 할당 연산자를 명시 적으로 구매하거나 컴파일러가 암시 적으로 생성합니다. 이렇게하면 사본이 나옵니다.

Move semantics는 기본적으로 r 값 참조 (&& (예 2 앰퍼샌드)를 사용하는 새로운 유형의 참조)를 사용하는 생성자가있는 사용자 정의 유형입니다.이 연산자를 const가 아닌 이동 생성자라고하며 할당 연산자에도 동일하게 적용됩니다. 그래서 이동 생성자는 무엇을합니까? 메모리를 복사하는 대신 소스 인수를 사용하여 메모리를 원본에서 대상으로 이동합니다.

언제 그걸하고 싶니? 잘 std :: vector 예를 들어, 당신이 만든 임시 std :: vector 및 함수에서 반환 말할 :

std::vector<foo> get_foos();

함수가 반환 될 때 복사 생성자로부터 오버 헤드가있을 것입니다. (그리고 C ++ 0x에서) std :: vector에는 복사 대신 이동 생성자가 있습니다. 포인터를 설정하고 동적으로 '이동'을 설정할 수 있습니다 새 인스턴스에 메모리. 그것은 std :: auto_ptr에서 소유권 이전과 같은 의미를가집니다.


이동 의미론에 대한 깊이 있고 깊이있는 설명에 관심이 있다면 "C ++ 언어 로 이동 의미 체계 지원을 추가하라는 제안" 이라는 원본 문서를 읽는 것이 좋습니다 .

그것은 매우 접근하기 쉽고 읽기 쉽고 그들이 제공하는 이점에 대한 훌륭한 사례입니다. WG21 웹 사이트 에서 이용할 수있는 이동 시멘틱스에 관한 최근의 또 다른 최신 논문 이 있지만,이 기사 는 최상위 레벨보기에서 사물에 접근하고 껄끄 러운 언어 세부 정보를 많이 얻지 못하기 때문에 아마도 가장 간단 할 것입니다.





move-semantics