* 정의
std::move은 객체를 오른값(rvalue) 참조로 캐스팅하기 위해 사용된다.
단순히 std::move는 무조건 오른값으로 캐스팅, std::forward는 조건부 캐스팅이라고 생각하면 편하다.
* 사용 이유
주로 이동 연산의 진행을 목적으로 사용된다. 예를 들어 오른값을 매개변수로 받는 이동 생성자와 이동 대입 연산자에 기존 값을 오른값으로 캐스팅해서 넘겨줄 때 사용된다.
일반적으로 이동 연산이 복사보다 성능이 좋지만 항상 그렇지 않다. 또한 기대보다 성능을 많이 올리지 못할 수 있다.
* 동작 방식
- 기본 동작 방식
캐스팅을 수행하는 함수 템플릿으로 std::move는 단순히 오른값 참조를 반환하는 역할을 수행한다.
// 매개변수는 보편 참조지만(오른값이 들어올 수도 왼값이 들어올 수도 있다.) 무조건 오른값 참조를 반환한다.
// C++ 11버전 move
template<typename T>
typename std::remove_reference<T>::type&& move_11(T&& param)
{
return static_cast<std::remove_reference<T>::type&&>(param);
}
// C++ 14버전 move
template<typename T>
decltype(auto) move_14(T&& param)
{
return static_cast<std::remove_reference_t<T>&&>(param);
}
매개변수는 보편 참조지만(오른값이 들어올 수도 왼값이 들어올 수도 있다.) 무조건 오른값 참조를 반환한다.
- std::move와 이동 연산
std::move 함수는 직접 이동을 수행하는 것은 아니며 이동할 수 있는 객체를 좀 더 쉽게 지정하기 위한 함수라는 점에서 move라는 이름이 붙은 것이다. std::move는 단순히 오른값으로의 캐스팅만을 수행하며 그 뒤 이동 연산은 이동 생성자나 이동 대입 연산자가 수행한다.
move를 통해 이동할 수 있는 객체를 만들고 이동 연산을 진행하면 먼저 얕은 복사를 진행하고 정의되지 않았지만 유효한 상태로 만들어주는 순서로 진행된다. 정의되지 않았지만 유효한 상태란 클래스마다 다르며 예를 들어 std::string의 경우 빈 문자열로 변경되고, std::unique_ptr의 경우 널로 초기화 된다.
Is move semantics just a shallow copy and setting other's pointers to null?
I've been reading about move semantics in C++, and in the explanations people give a lot of analogies to help simplify it, and in my head all I can see is that what people call "moving" as opposed ...
stackoverflow.com
MyString이라는 클래스를 직접 만들어 예를 들어보면 다음과 같다.
class MyString
{
public:
// 일반 생성자
MyString(const char* input) noexcept
{
// 기본 생성 과정
}
// 복사 생성자
MyString(const MyString& input) noexcept
{
// input -> this 깊은 복사가 이루어진다.
}
// 대입 연산자
MyString& operator=(const MyString& input) noexcept
{
// input -> this 깊은 복사가 이루어진다.
return *this;
}
/* 이동 관련 연산을 진행하기 위해서는 매개변수를 리셋해야하기 때문에 매개변수에 const가 붙지 않는다. */
// 이동 생성자
MyString(MyString&& input) noexcept
{
// input -> this 이동이 일어난다.
// 이동을 수행한 객체는 정의되지 않았지만 유효한 상태로 만들어준다.
input.reset();
}
// 이동 대입 연산자
MyString& operator=(MyString&& input) noexcept
{
// input -> this 이동이 일어난다.
// 이동을 수행한 객체는 정의되지 않았지만 유효한 상태로 만들어준다.
input.reset();
return *this;
}
public:
void reset(void) noexcept
{
// 정의되지 않았지만 유효한 상태로 만들어준다.
// string에서는 빈 문자열로 초기화되는 것이 일반적이다.
}
public:
// 문자열을 구성하는데 필요한 버퍼, 인덱스 등
};
- 함수의 반환 값과 std::move, std::forward
std::move를 사용하여 이동 생성자나 이동 복사 연산자를 호출하게 되면 성능이 더 향상될 수 있다. 문자열 클래스같은 경우에는 버퍼를 새로 할당하지 않고 포인터만 옮겨줌으로써 이를 실현한다.
그러면 함수에서 반환값을 반환할 때도 무조건 std::move나 std::forward를 통해서 이동 생성을 통해 최적화할 수 있지 않을까?
결론적으로 함수에서 지역변수나 값 전달 방식의 함수의 매개변수를 반환할 때 std::forward나 std::move로 반환하면 안된다. 그 이유는 컴파일러 최적화나 반환 값이 취급되는 방식 때문이다.
먼저 내부적으로 반환 될 지역변수를 반환값을 위해 마련된 메모리 안에 생성하여 반환시 복사를 제거하는 최적화(반환값 최적화)를 수행하는 컴파일러가 있다. 만약 이러한 최적화를 하고 있지 않다고 해도 반환되는 객체는 오른값으로 취급되어야 한다고 나와있기 때문에 알아서 적용해준다.(return rv;를 return std::move(rv); 와 같이 처리해준다.) 따라서 반환값 최적화는 신경 안쓰고 그냥 반환하면 된다.
* 주의 사항
1. const ClassName&& 을 매개변수로 가진 이동 연산은 없다. 이동 연산을 사용하기 위해서는 무조건 non-const 형식을 사용해야한다. const 객체는 std::move을 사용하면 안된다.
class Annotation
{
public:
Annotation(const MyString& text)
: _text(std::move(text)) {}
// ...
public:
MyString _text;
};
위의 코드에서 std::move는 const 형에 대해서 std::move를 호출하는 것은 의미가 없다. (실제로 사용하지 말라고 쓰여있다.) 무조건 반환형이 non-const 형이여야 이동 관련 연산을 수행할 수 있기 때문이다.
여기서 move 연산의 경우 const MyString&& 형식인데 이는 이동 생성자의 매개변수가 아니다. const에 대한 왼값 참조를 const에 대한 오른값에 묶는 것이 허용되기 때문에 최종적으로 복사 생성자인 MyString(const MyString& input)가 호출된다.
2. 오른값 참조 형식인 왼값은 왼값이다. 혼동하지 말자. 주소를 취할 수 있으면 왼값, 그렇지 않으면 오른값이다.
class Annotation
{
public:
Annotation(MyString&& text)
: _text(text) {}
// ...
public:
MyString _text;
};
text 변수는 오른값 참조이지만 text 그 자체는 왼값이다. 따라서 복사 생성자인 MyString(const MyString& input)가 호출된다.
정상적으로 이동 생성자가 호출되는 코드는 다음과 같다.
class Annotation
{
public:
// 이동 생성을 수행한다.
Annotation(MyString&& text)
: _text(std::move(text)) {}
// ...
public:
MyString _text;
};
'C++' 카테고리의 다른 글
| 보편 참조와 오른값 참조 (0) | 2022.03.27 |
|---|---|
| std::forward (0) | 2022.03.27 |
| 스마트 포인터 - std::weak_ptr (0) | 2022.03.23 |
| 스마트 포인터 - std::shared_ptr (0) | 2022.03.22 |
| 스마트 포인터 - std::unique_ptr (0) | 2022.03.22 |