-
2. std::moveModern 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);
}'Modern C++ > Move Semantics' 카테고리의 다른 글
5. C++11 객체 생성 규칙 Rule of Five (0) 2020.07.01 4. 이동가능 객체(Movable Type) (0) 2020.06.30 3. 완벽 전달(Perfect Forwarding) (1) 2020.06.29 1. 이동 의미론(Move Semantics) (1) 2020.06.29