이동 의미론(Move Semantics)과 완벽 전달(Perfect Forwarding)
Move Semantics: 비싼 복사 연산을 덜 비싼 이동 연산으로 대체할 수 있다.
Perfect Forwarding: 임의의 인수들을 받아서 다른 함수로 전달하는 함수를 작성할 때, 정확히 같은 타입의 인수로 전달할 수 있다.
1. std::move와 std::forward를 숙지한다.
std::move는 주어진 인수를 무조건 오른값으로 캐스팅하고, std::forward는 특정 조건이 만족될 때에만 그런 캐스팅을 수행한다.
- std::move의 구현
namespace std
{
// C++ 11
template<typename T>
typename remove_reference<T>::type&& move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
// C++ 14
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
}
1. move 함수는 && 참조를 받는다.
2. 참조를 제거한 타입 remove_reference<T>::type으로 부터 r-value 참조자 && 타입으로 캐스팅한다.
3. 반환 타입은 무조건 오른값이다.
std::move는 캐스팅을 수행할 뿐, 이동(move)는 수행하지 않는다.
- const 매개 변수를 move할 때
class Annotation
{
private:
std::string value;
public:
explicit Annotation(const std::string text)
:
value(std::move(text))
{}
};
std::move(text)의 결과는 const std::string &&, 즉 const 오른값 참조가 된다.
class string
{
public:
string(const string& rhs); // const 오른값은 const 왼값 참조에 묶인다.
string(string&& rhs);
};
const std::string&& 형식의 rhs는 함수의 오버로딩 과정에서 가장 맞는 함수를 선택하게 된다. 이 과정에서 const 매개변수를 받는 복사 생성자가 선택되는데, const 오른값 참조&&는 const 왼값 참조&에 묶일 수 있기 때문이다. 때문에 const 매개변수로 받는 생성자의 move는 이동 생성자가 오버로딩되지 않는다.
이동을 지원할 객체에 대해 const 매개 변수로 받지 말아야하며 std::move는 r-value 참조에 묶일 수 있도록만 해주고 실제 이동 자격을 갖게되는 것은 아니다.
- std::forward는 조건부 캐스팅
std::move는 주어진 인수를 무조건 오른값으로 캐스팅하지만, std::forward는 특정 조건이 만족될 때에만 캐스팅한다.
void process(const Widget& lvalArg)
{
printf("lvalue\n");
}
void process(Widget&& rvalArg)
{
printf("rvalue\n");
}
template<typename T>
void logAndProcess(T&& param)
{
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
// 항목 23
Widget w;
logAndProcess(w); // 왼값 호출
logAndProcess(std::move(w)); // 오른값 호출
자신의 내부에서 logAndProcess는 주어진 param을 함수 process에 "전달"한다. param이 오른값이라면 오른값으로 전달하고 왼값이라면 왼값으로 전달한다. 따라서 실제 param의 타입에 따라 다른 함수가 오버로딩된다.
std::forward는 템플릿 인수 T를 참고하여 오른값 인수에 대해서만 오른값으로 캐스팅한다.
class Widget
{
public:
Widget() {}
Widget(Widget&& rhs)
:
s(std::move(rhs.s))
{
++moveCtorCalls;
}
private:
static std::size_t moveCtorCalls;
std::string s;
};
class Widget
{
public:
Widget() {}
Widget(Widget&& rhs)
:
s(std::forward<std::string>(rhs.s)) // 관례적으로 안쓰는 표현
{
++moveCtorCalls;
}
private:
static std::size_t moveCtorCalls;
std::string s;
};
std::move를 쓸 수 있는 곳에 std::forward를 쓸 수 있지만 사용자가 명시적으로 std::move를 이용하여 이동을 수행하는 경우 std::move로 명시하는 것이 바람직하다.
- 정리
- std::move는 오른값으로 무조건 캐스팅한다.
- std::forward는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다.
- std::move와 std:forward 둘 다 실행 시점에서 아무일도 하지않는다.
2. 보편 참조와 오른값 참조를 구별한다.
// 항목 24
void f(Widget && param); // 오른값 참조
Widget&& var1 = Widget(); // 오른값 참조
auto&& var2 = var1; // 오른값 참조 아님
template<typename T>
void f(std::vector<T> && param); // 오른값 참조
template<typename T>
void f(T && param); // 오른값 참조 아님
"T&&"의 의미는 오른값 참조 또는 왼값 참조 중 하나이다. 모든 타입을 참조 형태로 묶을 수 있는데 (const lvalue -> const &, volatile -> volatile&, const rvalue -> const &&), 이를 보편 참조라고 한다.
- 보편 참조
template<typename T>
void f(T&& param); // param은 보편 참조
auto&& var2 = var1; // var2는 보편 참조
두 문맥의 공통점은 형식 연역이 일어난다. 템플릿에서는 param에 대한 형식이 연역되고 var2 선언에서는 var2의 형식이 연역된다.
- 형식 연역이 일어나지 않는 경우
void f(Widget && param); // 오른값 참조
Widget&& var1 = Widget(); // 오른값 참조
형식 연역이 일어나지 않는 경우 보편 참조가 아니므로 항상 오른값 참조이다.
- 보편 참조는 형식 연역이 일어난다.
template<typename T>
void f(T && param); // param 보편 참조
Widget w;
f(w); // 왼값 -> 왼값 참조로 연역
f(std::move(w)); // 오른값 -> 오른값 참조로 연역
- 보편 참조의 형태에 관해
<T>&& 인수로 들어간 경우, 단지 <T> 인자로 결정되는 클래스에 대한 && 오른값을 지칭한다.
template<typename T>
void f(std::vector<T> && param); // 오른값 참조
// 왼값을 오른값에 묶을수 없음
std::vector<int> v;
f(v);
const 한정사가 붙으면 보편 참조가 아니다.
template<typename T>
void f(const T && param); // param은 오른값 참조
템플릿 클래스의 템플릿 함수가 아닌 함수 선언 중 매개변수 타입이 T&& 인 경우
template<class T, class Allocator = allocator<T>>
class vector
{
public:
void push_back(T&& x);
};
push_back의 매개변수 T&&는 인스턴스 형식에 의해 완전 결정되는 부분이다.
std::vector<Widget> v; // 템플릿 클래스의 인스턴스화
class vector<Widget, allocator<Widget>>
{
public:
void push_back(Widget&& x);
};
- 템플릿 클래스안 템플릿 함수
Args는 템플릿 인수 T와 독립적인 템플릿 인수이기 때문에 Args&& 보편 참조 형태이다. (paramter pack로 이루어진)
template<class T, class Allocator = allocator<T>>
class vector {
public:
template<class... Args>
void emplace_back(Args&&... args);
};
- 정리
- 함수 템플릿 매개변수의 형식이 T&& 형태이고 T가 연역된다면, 또는 객체를 auto&&로 선언한다면 그 매개변수나 객체는 보편 참조이다.
- 형식 선언 형태가 정확히 T&& 가 아니면, 또는 형식 연역이 일어나지 않으면 T&&는 오른값 참조를 뜻한다.
- 오른값으로 초기화되는 보편 참조는 오른값 참조에 해당한다. 왼값으로 초기화되는 보편 참조는 왼값 참조에 해당한다.
3. 오른값 참조에는 std::move를 보편 참조에는 std::forward를 사용한다.
- 오른값 참조가 확실한 매개변수의 경우 명시적으로 오른값을 이용할 수 있는 std::move를 선호하도록 한다.
class Widget
{
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
public:
Widget(Widget&& rhs) // rhs는 오른값 참조
:
name(std::move(rhs.name)), // 이동 수행
p(std::move(rhs.p))
{}
};
- 보편 참조의 경우 해당 매개변수가 왼값인지 오른값인지 모르기 때문에 선택적으로 캐스팅되도록 한다.
class Widget
{
public:
template<typename T>
void SetName(T&& newName)
{
name = std::forward<T>(newName);
}
};
보편 참조 매개변수에 std::move를 사용할 경우
class Widget
{
public:
template<typename T>
void SetName(T&& newName)
{
name = std::move(newName);
}
};
std::string getWidgetName();
Widget w;
auto n = getWidgetName(); // n은 지역 변수
w.setName(n); // n을 w로 이동한다.
// n ???
n은 왼값이지만 move에 의해 SetName안에서 강제 이동되었고 n은 미지정 값이 된다.
보편 참조 대신 두개의 오버로딩을 사용
class Widget
{
public:
void setName(const std::string& newName)
{
name = newName;
}
void setName(string&& newName)
{
name = std::move(newName)
}
};
w.setName("Adela Novak");
const char[12] 형식의 "Adela Novak"를 매개 변수로 사용할 경우, 두번째 함수 즉 오른값 참조를 받는 함수가 선택될 것이다. 이 때 일어나는 과정은 다음과 같다.
1. 임시 string 객체가 생기며 string&& newName에 묶인다.
2. 이동 연산이 호출된다.
3. 임시 string 객체가 파괴된다.
vs 보편 참조와 std::forward
class Widget
{
public:
template<typename T>
void SetName(T&& newName)
{
name = std::forward<T>(newName);
}
};
const char[12]&& newName로 바로 전달되고 이동 연산의 매개변수로 쓰이게된다.
보편 참조를 쓸 경우 매개 변수 수에 제약이 덜 하다.
template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);
스마트 포인터의 팩토리 함수는 Args&& 보편 참조의 파라미터 팩을 받는다.
- std::move와 std::forwar의 효율적인 사용법
객체를 사용하기 전 move나 forward 호출로 이동되는 것을 막는다.
template<typename T>
void setSignText(T&& text)
{
sign.setText(text); // 수정하지는 않는다.
auto now = std::chrono::system_clock::now();
signHistory.add(now, std::forward<T>(text)); // 나중에 오른값으로 조건부 캐스팅
}
- 함수가 값으로 돌려주고 그것이 오른값 참조나 보편 참조에 묶인 객체라면 return 문에서 std::move나 std::forward를 쓰는 것을 고려한다.
값으로 돌려주고 오른값 참조에 묶인 경우, 수학 연산 라이브러리의 operator+의 예제
Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // lhs는 이동한다.
}
Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return lhs; // lhs는 복사된다.
}
lhs는 연산자의 왼쪽에 오기때문에 보통 연산 후 사용되지 않는다. 그것이 게다가 오른값 참조라면 연산후 결과로 이동시키는 것에 문제가 없다. 만약 std::move를 사용하지 않는다면, lhs는 왼값이므로 결과값에 대해 복사가 일어난다.
값으로 돌려주고 보편 참조에 묶인 경우
template<typename T>
Fraction reduceAndCopy(T&& frac)
{
frac.reduce();
return std::forward<T>(frac);
}
frac이 오른값인 경우 이동하고 왼값의 경우 복사한다.
- 함수가 값을 반환하고 반환될 객체가 지역 변수인 경우 std::move가 의미가 없다.
Widget makeWidget()
{
Widget w;
//...
return w;
}
Widget makeWidget()
{
Widget w;
// ...
return std::move(w);
}
반환값 최적화(return value optimization, RVO)에 의해 지역 변수 w를 함수의 반환값을 위해 마련한 메모리 안에 생성한다면 w의 복사를 피할 수 있다.
그 조건은 1) 지역 객체의 형식이 함수의 반환 형식과 같아야하고 2) 지역객체가 바로 함수의 반환값이어야 한다.
Widget makeWidget()
{
Widget w; // 지역 변수와 반환값이 타입과 일치
//...
return w; // 복사가 일어날 것 같지만 RVO에 의해 복사가 일어나지 않는다.
}
Widget makeWidget()
{
Widget w; // 지역 변수와 반환값이 타입과 일치
// ...
return std::move(w); // 하지만 그 실제 반환값은 Widget&&임으로 불일치 한다.
}
따라서 첫번쨰 makeWidget()은 반환값 최적화로 복사가 일어나지 않는다. 두번째 makeWidget()은 최적화의 시도가 오히려 RVO를 제한하여 이동이 실패할 경우 복사가 일어난다.
RVO를 하지 않는다고 해도 반환값은 컴파일러가 알아서 std::move(w)로 취급하기 때문에 return에 std::move를 쓰지 않는다.
// RVO를 하지 않는 경우
Widget makeWidget(Widget w) // 함수의 매개변수가 값이고 이것이 반환 될 경우
{
//...
return w; // 오른값으로 취급한다.
}
// 같은의미
Widget makeWidget(Widget w)
{
return std::move(w);
}
- 정리
- 오른값 참조나 보편 참조가 마지막으로 쓰이는 지점에서 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 적용한다.
- 결과를 값 전달 방식으로 돌려주는 함수가 오른값 참조나 보편 참조를 돌려줄 때에도 각각 std::move나 std::forward를 적용한다.
- 반환값 최적화의 대상이 될 수 있는 지역객체에는 std::move와 std::forward를 적용하지 않는다.
'Advanced C++' 카테고리의 다른 글
[Modern C++] (6) 람다 표현식 (0) | 2021.04.20 |
---|---|
[Modern C++] (5-2) 오른값 참조, 이동 의미론, 완벽전달 (0) | 2021.04.14 |
[C++] 형 변환 (2) dynamic_cast / static_cast / reinterpret_cast / const_cast (0) | 2021.04.01 |
[C++] 형 변환 type casting (0) | 2021.04.01 |
[Modern C++] (3-2) 현대적 C++에 적응하기 (0) | 2021.03.26 |