ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 2. std::move
    Modern C++/Move Semantics 2020. 6. 29. 17:33

    오른쪽 값(R-value)는 외부에서 구별할 수 있는 identity가 없는 임시 객체로 해당 객체의 자원의 소유권을 포기할 수 있다고 생각할 수 있다. 

    int foo() {
        return 5;
    }

    foo(); // 함수 호출 시 int형 임시 값이 생성되지만 코드 범위를 벗어나는 경우 자동 소멸
    std::vector<int> v0 { 1, 2, 3, 4, 5 };

    auto v1  = v0; // v1 의 복사 생성자 호출

    위와 같이 벡터 v1을 v0를 통해서 초기화하고 생성하는 경우 v1의 v0의 내부 버퍼를 강제적으로 복사하게 된다. 

    std::vector<int> get_vector {
        return std::vector<int> {1, 2, 3, 4, 5};
    }

    auto v2 = get_vector();
    // 함수 호출 시 임시 vector 객체가 생성되고 해당 값이 r-value 이므로
    // 컴파일러는 std::vector<int>(std::vector<int>&& ) 인 이동 생성자를 호출 하게 된다.
    // 따라서 v2는 추가적인 자원 복사 대신 임시 객체에 대한 소유권을 이전 받게 된다.  

    위처럼 자원의 소유권 이전을 객체의 이동이라고 부르고 이는 std::vector<int>&& 와 같은 오른값 참조와 관계된다. 

     

    v0가 해당 범위에서 더 이상 사용되지 않는 다는 걸 알고 있고 있다면 v1 생성 시 자원을 복사하는 대신 v0의 소유권을 포기 시키고 객체를 이동할 방법이 필요하게 된다. 이를 위해 C++ 에서는 std::move() 함수를 지원한다. 

     

    1. std::move()

    함수 정의 

    template< class T >
    constexpr typename std::remove_reference<T>::type&& move( T&& t ) noexcept;                      (since C++14)
    Return Value
    static_cast<typenam std::remove_reference<T>::type&&>(t)
    std::move 의 파라미터로 전달된 T&& t 는 오른 값 참조가 아니고 보편 참조로 왼값 참조, 오른값 참조 모두 가능 

     

    std::move는 실제 이동을 시키지 않고 단지 파라미터를 해당 타입의 오른쪽 값 참조로 static_cast 한다.

    이는 이동 가능(move-aware)한 객체를 통해 실제 객체 이동이 발생한다. 

    std::vector<int> v0 { 1, 2, 3, 4, 5 }

    auto v1 = std::move(v0); // std::vector<int> v1(std::move(v0)); 와 동일 
    //std::move(v0) 는 std::vector<int>&& 형으로 변환되었으므로 vector(vector&& other) 인 이동 생성자가 호출됨
    // 이동 생성자에서는 v0의 소유권을 v1으로 이전(객체 이동)

    auto v2 = static_cast<std::vector<int>&&>(v1);
    //실제 std::move 는 위와 같이 해당 타입의 오른 값 참조로 형 변환하는 것과 동일 

    주의할점은 위에서 언급한 것 처럼 v0와 v1은 더 이상 해당 객체에 대해 소유권이 없고 v0나 v1 객체를 사용하면 정의되지 않는 동작을 수행하게 된다. 따라서 해당 객체가 이동하면 이동된 객체는 더 이상 사용하지 않도록 한다. 

    또한 성능 개선을 위해 함수 반환 값으로 내부 로컬 객체를 반환 할 때 std::move() 를 통해서 값을 반환하지 않도록 해야한다. 컴파일러는 자체적으로 함수 내 로컬 객체를 반환하는 경우에는 RVO(Return Value Optimaization)를 통해 반환 값의 최적화를 수행하는 데 std::move()를 사용할 경우 반환 값 최적화가 일어나지 않아 성능 향상에 오히려 해를 줄 수 있다. 

    std::vector<std::string> get_names() {
        std::vector<std::string> names { "Black Pink", "BLUE", "IU", "Oh My Girls" };
        //return std::move(names); //함수 내에서 생성된 로컬 객체 names 를 이동 반환 하지 말것. 
        return names;
    }
    template <typename T>
    std::ostream& operator << (std::stream& oss, const std::vector<T>& v) {
        oss.put('{');
        char delimeter[3] = {'\0', ' ' , '\0'};
        for(const auto& e : v) {
            oss << comma << e;
            comma[0] = ',';
        }
        return oss << '}';
    }

    void noop() {
        std::vector<int> v0{1, 2, 3, 4, 5}
        std::move(v0); // No-op;
        std::cout << v0 << std::endl; //v0는 이동되지 않았으므로 사용 가능 
    }

    std::move(v0)는 실제 이동이 일어나지 않고 오른쪽 값 참조로 강제로 타입 변환만 일어난다는 점을 꼭 기억해야 한다. 

    위는 std::move(v0)는 아무 일도 일어나지 않고 std::move() 호출 되었다 하더라도 실제 이동이 일어나지 않았다면 std::move() 호출 이후에도 v0 객체를 사용할 수 있음을 보여준다. 

     


    2. STL 의 이동 연산 지원 

    STL 컨테이너나 유틸리티 객체들은 모두 이동 연산을 지원한다. 여기서는 그 중 몇 가지를 살펴 본다. 

     

    범용 클래스에서 이동 연산 지원

    //범용 유틸 클래스 pair
    std::pair<std::string, int> ages_pair {"IU", 1993};
    auto copy_pair = ages_pair; //복사 생성자 호출
    auto move_pair = std::move(ages_pair); //이동 생성자 호출
    std::cout << copy_pair.first << ":" << copy_pair.second << std::endl;
    //ages_pair는 이동된 객체로 접근 시 정의되지 않는 동작 수행
    std::cout << ages_pair.first << ":" << ages_pair.second << std::endl;


    //
    범용 유틸 클래스 tuple
    std::tuple<std::string, std::string, std::string> address_tuple =
    {"130Gil Tehran-ro", "Gangnam-Gu", "Seoul"};

    auto copy_tuple = address_tuple;
    auto move_tuple = std::move(address_tuple);

    std::cout << std::get<0>(copy_tuple) << std::endl;
    //address_tupe은 이동된 객체로 접근 시 정의되지 않은 동작 수행
    std::cout << std::get<0>(address_tuple) << std::endl;
    std::cout << std::get<0>(move_tuple) << std::endl;

    STL 의 컨테이너 객체 

    STL 컨테이너 객체는 컨테이너 객체 전체가 이동 가능하고, 아이템을 컨테이너 내부로 이동 시킬 수 도 있다. 

     std::vector<std::string> names{"jone Doe", "Jane Doe" } ;
     auto copy_vector = names;
     auto move_vector = std::move(names);
     // names 객체는 이동되었으므로 더 이상 사용 불가

    //임시 객체를 내부로 이동
    move_vector.push_back("Tom");

    std::string name = "Silvia";
    // name을 복사해서 내부로 이동.
    move_vector.push_back(name);

    // name을 벡테 내부로 이동 
    move_vector.push_back(std::move(name));
    //name 객체를 이동했으므로 사용하면 안됨
     std::cout << move_vector << std::endl;

    STL 내에는 복사는 되지 않고 이동만 가능한 클래스들도 존재한다. 대표적인 예로 std::thread, std::unique_lock,

    std::unique_ptr등이 있다. 이는 해당 클래스의 소유권이 오직 한 곳에만 존재함을 나타낸다. 

    std::thread t([] { std::cout << "Hello world"; });
    //auto copy_t = t // 컴파일 불가
    auto move_t = std::move(t);
    move_t.join();

    {
        std::mutex m;
        std::unique_lock<std::mutex> lock(m);
       auto move_lock = std::move(lock);
    }


    {
       std::unique_ptr<int> ptr_a = std::make_unique<int>(0);
       auto move_ptr = std::move(ptr_a);
    }

     

    댓글

Designed by Tistory.