수색…


의미 이동

이동 의미는 C ++에서 한 객체를 다른 객체로 이동시키는 방법입니다. 이를 위해 이전 객체를 비우고 새로운 객체에 있던 모든 것을 배치합니다.

이를 위해 우리는 rvalue reference가 무엇인지 이해해야합니다. rvalue 참조 ( T&& T는 객체 유형 임)는 일반적인 참조 ( T& , lvalue 참조라고 함) T& 크게 다르지 않습니다. 그러나 그것들은 2 개의 서로 다른 타입으로 동작하기 때문에 하나의 타입이나 다른 타입을 취하는 생성자 나 함수를 만들 수 있습니다. 이것은 이동 시멘틱스를 다룰 때 필요합니다.

두 가지 유형이 필요한 이유는 두 가지 다른 행동을 지정하는 것입니다. Lvalue 참조 생성자는 복사와 관련이 있고 Rvalue 참조 생성자는 이동과 관련이 있습니다.

객체를 이동하기 위해 std::move(obj) 합니다. 이 함수는 객체에 대한 rvalue 참조를 반환하므로 객체의 데이터를 새로운 객체로 도용 할 수 있습니다. 아래에서 설명하는 여러 가지 방법이 있습니다.

중요한 점은 std::move 사용하면 rvalue 참조가 만들어집니다. 즉, std::move(obj) 문은 obj의 내용을 변경하지 않지만 auto obj2 = std::move(obj) (가능)은 자동으로 처리합니다.

생성자 이동

이 코드 스 니펫이 있다고 가정 해보십시오.

class A {
public:
    int a;
    int b;
       
    A(const A &other) {
        this->a = other.a;
        this->b = other.b;
    }
};

복사 생성자를 만들려면 즉, 객체를 복사하고 새로운 객체를 만드는 함수를 만들려면 일반적으로 위에 표시된 구문을 선택합니다. A 유형의 다른 객체에 대한 참조를 취하는 A의 생성자를 사용합니다. 메소드 내부에서 객체를 수동으로 복사합니다.

또는 A(const A &) = default; 쓸 수 있습니다 A(const A &) = default; 모든 멤버를 자동으로 복사하여 복사 생성자를 사용합니다.

그러나 이동 생성자를 만들려면 여기서와 같이 왼쪽 값 참조 대신 오른쪽 값 참조를 사용합니다.

class Wallet {
public:
    int nrOfDollars;
    
    Wallet() = default; //default ctor

    Wallet(Wallet &&other) {
        this->nrOfDollars = other.nrOfDollars;
        other.nrOfDollars = 0;
    }
};

이전 값을 zero 설정했음을 유의하십시오. 기본 이동 생성자 ( Wallet(Wallet&&) = default; )는 POD이므로 nrOfDollars 의 값을 복사합니다.

이동 의미론은 원본 인스턴스에서 '도용'상태를 허용하도록 설계되었으므로 원래 인스턴스가이 도용 후에 어떻게 보이는지를 고려하는 것이 중요합니다. 이 경우 값을 0으로 변경하지 않으면 달러를 두 배로 늘릴 수 있습니다.

Wallet a;
a.nrOfDollars = 1;
Wallet b (std::move(a)); //calling B(B&& other);
std::cout << a.nrOfDollars << std::endl; //0
std::cout << b.nrOfDollars << std::endl; //1

따라서 우리는 이전 객체로부터 객체를 구성했습니다.


위의 예제는 간단한 예제이지만 이동 생성자가 수행 할 작업을 보여줍니다. 자원 관리와 관련된보다 복잡한 경우에 더 유용합니다.

    // Manages operations involving a specified type.
    // Owns a helper on the heap, and one in its memory (presumably on the stack).
    // Both helpers are DefaultConstructible, CopyConstructible, and MoveConstructible.
    template<typename T,
             template<typename> typename HeapHelper,
             template<typename> typename StackHelper>
    class OperationsManager {
        using MyType = OperationsManager<T, HeapHelper, StackHelper>;

        HeapHelper<T>* h_helper;
        StackHelper<T> s_helper;
        // ...

      public:
        // Default constructor & Rule of Five.
        OperationsManager() : h_helper(new HeapHelper<T>) {}
        OperationsManager(const MyType& other)
          : h_helper(new HeapHelper<T>(*other.h_helper)), s_helper(other.s_helper) {}
        MyType& operator=(MyType copy) {
            swap(*this, copy);
            return *this;
        }
        ~OperationsManager() {
            if (h_helper) { delete h_helper; }
        }

        // Move constructor (without swap()).
        // Takes other's HeapHelper<T>*.
        // Takes other's StackHelper<T>, by forcing the use of StackHelper<T>'s move constructor.
        // Replaces other's HeapHelper<T>* with nullptr, to keep other from deleting our shiny
        //  new helper when it's destroyed.
        OperationsManager(MyType&& other) noexcept
          : h_helper(other.h_helper),
            s_helper(std::move(other.s_helper)) {
            other.h_helper = nullptr;
        }

        // Move constructor (with swap()).
        // Places our members in the condition we want other's to be in, then switches members
        //  with other.
        // OperationsManager(MyType&& other) noexcept : h_helper(nullptr) {
        //     swap(*this, other);
        // }

        // Copy/move helper.
        friend void swap(MyType& left, MyType& right) noexcept {
            std::swap(left.h_helper, right.h_helper);
            std::swap(left.s_helper, right.s_helper);
        }
    };

과제 이동

lvalue 참조가있는 객체에 값을 할당하고 복사하는 것과 마찬가지로 새 값을 작성하지 않고 객체의 값을 다른 객체로 이동할 수도 있습니다. 우리는이 이동 할당이라고 부릅니다. 한 객체에서 다른 기존 객체로 값을 이동합니다.

이를 위해 operator = 를 오버로드해야합니다. 복사 할당과 마찬가지로 좌변 값 참조를 사용하지 않고 rvalue 참조를 사용합니다.

class A {
    int a;
    A& operator= (A&& other) {
        this->a = other.a;
        other.a = 0;
        return *this;
    }
};

이것은 이동 할당을 정의하는 일반적인 구문입니다. 우리는 operator = 오버로드하여 우변 값 참조에 피드를 제공 할 수 있고 그것을 다른 객체에 할당 할 수 있습니다.

A a;
a.a = 1;
A b;
b = std::move(a); //calling A& operator= (A&& other)
std::cout << a.a << std::endl; //0
std::cout << b.a << std::endl; //1

따라서 우리는 객체를 다른 객체에 할당 할 수 있습니다.

std :: move를 사용하여 O (n²)에서 O (n)까지의 복잡성을 줄입니다.

C ++ 11은 객체 이동을 위한 핵심 언어 및 표준 라이브러리 지원을 도입했습니다. 아이디어는 O를 논리적으로 빈하지만 여전히 파괴 및 복사 가능한 떠나, 객체 O를 임시을하고 때 하나의 논리적 사본을 원하는 것과 같은 동적으로 할당 된 버퍼처럼 빼내다 오의 리소스에 대한 다음의 안전합니다.

핵심 언어 지원은 주로

  • 를 rvalue 참조 유형 빌더 && 예는, std::string&& 에를 rvalue 참조입니다 std::string 이 개체를 언급하는 것이 그 자원을 단지 슬쩍 할 수있는 임시이다 나타내는 (즉, 이동)

  • 실제로 리소스를 복사하는 대신 지정된 다른 객체에서 리소스를 효율적으로 이동하기로되어있는 이동 생성자 T( T&& ) 대한 특수 지원 및

  • 이동 할당 연산자 auto operator=(T&&) -> T& 위한 특별 지원. 소스에서도 이동해야합니다.

표준 라이브러리 지원은 주로 <utility> 헤더에서 std::move 함수 템플리트입니다. 이 함수는 지정된 객체에 대한 rvalue 참조를 생성합니다. 이는 마치 임시 객체처럼 이동 될 수 있음을 나타냅니다.


컨테이너의 경우 실제 복사는 일반적으로 O ( n ) 복잡도이며, 여기서 n 은 컨테이너의 항목 수이고, 이동은 O (1), 일정 시간입니다. 그리고 그 용기 n 회 논리적 복사, 이는 일반적으로 실용적 O에서 복잡성을 줄일 수있는 알고리즘 (N ²) 단지 선형 O (N)이다.

Dr. Dobbs Journal의 2013 년 9 월 기사에서 "변경하지 않는 컨테이너"기사에서 Andrew Koenig는 초기화 후 변수가 불변 인 프로그래밍 스타일을 사용할 때 알고리즘 비효율의 흥미로운 예를 제시했습니다. 이 스타일 루프는 일반적으로 재귀를 사용하여 표현됩니다. Collatz 시퀀스를 생성하는 것과 같은 일부 알고리즘의 경우 재귀는 논리적으로 컨테이너를 복사해야합니다.

// Based on an example by Andrew Koenig in his Dr. Dobbs Journal article
// “Containers That Never Change” September 19, 2013, available at
// <url: http://www.drdobbs.com/cpp/containters-that-never-change/240161543>

// Includes here, e.g. <vector>

namespace my {
    template< class Item >
    using Vector_ = /* E.g. std::vector<Item> */;

    auto concat( Vector_<int> const& v, int const x )
        -> Vector_<int>
    {
        auto result{ v };
        result.push_back( x );
        return result;
    }

    auto collatz_aux( int const n, Vector_<int> const& result )
        -> Vector_<int>
    {
        if( n == 1 )
        {
            return result;
        }
        auto const new_result = concat( result, n );
        if( n % 2 == 0 )
        {
            return collatz_aux( n/2, new_result );
        }
        else
        {
            return collatz_aux( 3*n + 1, new_result );
        }
    }

    auto collatz( int const n )
        -> Vector_<int>
    {
        assert( n != 0 );
        return collatz_aux( n, Vector_<int>() );
    }
}  // namespace my

#include <iostream>
using namespace std;
auto main() -> int
{
    for( int const x : my::collatz( 42 ) )
    {
        cout << x << ' ';
    }
    cout << '\n';
}

산출:

42 21 64 32 16 8 4 2

벡터 복사로 인한 항목 복사 작업의 수는 대략 1 + 2 + 3 + ... n 이므로 대략 O ( )입니다.

구체적으로 g ++ 및 Visual C ++ 컴파일러에서 위의 collatz(42) 호출은 벡터 복사 생성자 호출에서 Collatz 시퀀스 8 collatz(42) 36 개의 항목 복사 연산 (8 * 7 / 2 = 28 및 일부)을 발생 시켰습니다.

이러한 항목 복사 작업의 모든 단순히 그 값이 더 이상 필요하지 않은 벡터를 이동하여 제거 할 수 있습니다. 이렇게하려면 벡터 형식 인수에 대한 const 및 참조를 제거하고 벡터 를 값별로 전달해야 합니다 . 함수 반환은 이미 자동으로 최적화되어 있습니다. 벡터가 전달되고 함수에서 더 이상 사용되지 않는 호출의 경우 실제로 복사하는 대신 std::move 를 적용하여 해당 버퍼를 이동하십시오 .

using std::move;

auto concat( Vector_<int> v, int const x )
    -> Vector_<int>
{
    v.push_back( x );
    // warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
    // See https://stackoverflow.com/documentation/c%2b%2b/2489/copy-elision
    // return move( v );
    return v;
}

auto collatz_aux( int const n, Vector_<int> result )
    -> Vector_<int>
{
    if( n == 1 )
    {
        return result;
    }
    auto new_result = concat( move( result ), n );
    struct result;      // Make absolutely sure no use of `result` after this.
    if( n % 2 == 0 )
    {
        return collatz_aux( n/2, move( new_result ) );
    }
    else
    {
        return collatz_aux( 3*n + 1, move( new_result ) );
    }
}

auto collatz( int const n )
    -> Vector_<int>
{
    assert( n != 0 );
    return collatz_aux( n, Vector_<int>() );
}

여기서 g ++ 및 Visual C ++ 컴파일러에서 벡터 복사 생성자 호출로 인한 항목 복사 작업의 수는 정확히 0입니다.

이 알고리즘은 여전히 반드시 생산 Collatz 시퀀스의 길이는 O (N)이지만, 이것은 매우 극적인 개선 : O (N ²) → O (N).


어떤 언어 지원을 사용하면 아마도 이동을 사용 하고 초기화 및 최종 이동 사이 에서 변수의 불변성을 표현하고 적용 할 수 있습니다. 후에 변수의 사용은 오류가되어야합니다. 아아, C ++ 14의 경우 C ++은이를 지원하지 않습니다. 루프 프리 코드의 경우, 이동 후 no use는 struct result; 와 같이 불완전한 struct 로 관련 이름의 재 선언을 통해 시행 될 수 있습니다 struct result; 그러나 이것은 추악하고 다른 프로그래머들에게는 이해하기 힘들다. 또한 진단은 오해의 소지가 있습니다.

요약하자면, C ++ 언어와 라이브러리의 이동 지원은 알고리즘의 복잡성을 대폭 개선하지만, const 가 제공 할 수있는 코드 정확성 보장 및 코드 명확성을 저버린 대가로 지원의 불완전 성으로 인해 가능합니다.


완전성을 위해 복사 생성자 호출로 인한 항목 복사 작업의 수를 측정하는 데 사용되는 계측 된 벡터 클래스 :
template< class Item >
class Copy_tracking_vector
{
private:
    static auto n_copy_ops()
        -> int&
    {
        static int value;
        return value;
    }
    
    vector<Item>    items_;
    
public:
    static auto n() -> int { return n_copy_ops(); }

    void push_back( Item const& o ) { items_.push_back( o ); }
    auto begin() const { return items_.begin(); }
    auto end() const { return items_.end(); }

    Copy_tracking_vector(){}
    
    Copy_tracking_vector( Copy_tracking_vector const& other )
        : items_( other.items_ )
    { n_copy_ops() += items_.size(); }

    Copy_tracking_vector( Copy_tracking_vector&& other )
        : items_( move( other.items_ ) )
    {}
};

컨테이너에 대한 이동 의미 사용

컨테이너를 복사하는 대신 이동할 수 있습니다.

void print(const std::vector<int>& vec) {
    for (auto&& val : vec) {
        std::cout << val << ", ";
    }
    std::cout << std::endl;
}

int main() {
    // initialize vec1 with 1, 2, 3, 4 and vec2 as an empty vector
    std::vector<int> vec1{1, 2, 3, 4};
    std::vector<int> vec2;

    // The following line will print 1, 2, 3, 4
    print(vec1);

    // The following line will print a new line
    print(vec2);

    // The vector vec2 is assigned with move assingment.
    // This will "steal" the value of vec1 without copying it.
    vec2 = std::move(vec1);

    // Here the vec1 object is in an indeterminate state, but still valid.
    // The object vec1 is not destroyed,
    // but there's is no guarantees about what it contains.

    // The following line will print 1, 2, 3, 4
    print(vec2);
}

이동 된 객체 재사용

이동 된 객체를 다시 사용할 수 있습니다.

void consumingFunction(std::vector<int> vec) {
    // Some operations
}

int main() {
    // initialize vec with 1, 2, 3, 4
    std::vector<int> vec{1, 2, 3, 4};

    // Send the vector by move
    consumingFunction(std::move(vec));

    // Here the vec object is in an indeterminate state.
    // Since the object is not destroyed, we can assign it a new content.
    // We will, in this case, assign an empty value to the vector,
    // making it effectively empty
    vec = {};

    // Since the vector as gained a determinate value, we can use it normally.
    vec.push_back(42);

    // Send the vector by move again.
    consumingFunction(std::move(vec));
}


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