이동 의미구조(Move Semantics) 주의할 점
CppCon 2019: Klaus Iglberger “Back to Basics: Move Semantics (part 2 of 2)” 내용 중 이동 의미구조(Move Semantics) 주의할 점 정리.
예제 1
1 2 3 4 5 6 7 8 9 10 |
class A { public: template <typename T> A(T&& t) : b_{std::move(t)} {} private: B b_; }; |
A(T&& t)
에서 t
는 전달 참조(forwarding reference)이므로 std::move
가 아니라 다음처럼 std::forward
를 사용해야 한다.
1 2 3 4 5 6 7 8 9 10 |
class A { public: template <typename T> A(T&& t) : b_{std::forward<T>(t)} {} private: B b_; }; |
예제 2
1 2 3 4 5 6 7 8 9 10 |
template <typename T> class A { public: A(T&& t) : b_{std::forward<T>(t)} {} private: B b_; }; |
A(T&& t)
는 클래스 템플릿의 생성자이고 T
는 클래스 템플릿 매개변수이다. 인스턴스를 생성할 때 타입을 지정하므로 T
는 타입 추론에 사용하지 않는다. 즉 t
는 오른값 참조이므로 다음처럼 std::move()
를 사용해야 한다.
1 2 3 4 5 6 7 8 9 10 |
template <typename T> class A { public: A(T&& t) : b_{std::move(t)} {} private: B b_; }; |
예제 3
1 2 3 4 5 6 7 8 9 10 11 12 |
class A { public: template <typename T> A(T&& t) : b_{std::forward<T>(t)}, c_{std::forward<T>(t)} {} private: B b_; C c_; }; |
A(T&& t)
에서 t
는 전달 참조(forwarding reference)이므로 std::forward
를 사용하는 것은 맞다. 하지만 std::forward
역시 조건부 이동을 하므로 같은 오른값을 두 번 이동할 수는 없다. 첫 번째는 다음처럼 복사한다.
1 2 3 4 5 6 7 8 9 10 11 12 |
class A { public: template <typename T> A(T&& t) : b_{t}, c_{std::forward<T>(t)} {} private: B b_; C c_; }; |
예제 4
1 2 3 4 5 6 7 8 9 10 11 12 |
class A { public: template <typename T1, typename T2> A(T1&& t1, T2&& t2) : b_{std::forward<T1>(t1)}, c_{std::forward<T2>(t2)} {} private: B b_; C c_; }; |
코드 자체는 문제가 없다. 다만 호출하는 쪽에서 t1
, t2
에 같은 오른값을 대입하면 문제가 될 뿐.
예제 5
1 2 3 4 5 6 |
template <typename... Args> std::unique_ptr<Widget> create(Args&&... args) { auto uptr{std::make_unique<Widget>(std::forward<Args>(args)...)}; return std::move(uptr); } |
return
에 std::move
를 사용해 이동하면 효율적일 것 같지만 RVO(Return Value Optimization, 반환값 최적화)를 하지 않으므로 오히려 좋지 않다. 다음처럼 바로 반환한다. 특히 C++17부터는 복사 생략(copy elision)을 표준으로 보장한다.
1 2 3 4 5 |
template <typename... Args> std::unique_ptr<Widget> create(Args&&... args) { return std::make_unique<Widget>(std::forward<Args>(args)...); } |
한 가지 더 주의할 점은 std::unique_ptr<Widget>&&
로 오른값 참조를 반환하지 않는다. 왼값 참조와 마찬가지로 이미 소멸한 객체를 참조하려 할 뿐이다.
예제 6
1 2 3 4 5 6 7 8 9 |
template <typename T> void foo(T&&) { if constexpr (std::is_integral_v<T>) { // Deal with integral types } else { // Deal with non-integral types } } |
foo()
에 int
형 왼값을 대입하면 T
는 int&
가 돼 의도한 대로 작동하지 않는다. 다음처럼 명시적으로 참조를 제거해야 한다.
1 2 3 4 5 6 7 8 9 10 |
template <typename T> void foo(T&&) { using NoRef = std::remove_reference_t<T>; if constexpr (std::is_integral_v<NoRef>) { // Deal with integral types } else { // Deal with non-integral types } } |
마지막으로 매개변수와 반환값을 전달할 때 권장 내용은 다음을 참고한다.