#include <utility>
std::move(x);
template <typename T>
constexpr typename std::remove_reference<T>::type&& move(T&& x) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(x);
} //실제 구현
std::move는 단지 x를 T&&로 static_cast 해주는 함수
→ “이 값은 이제 rvalue(xvalue)처럼 취급해도 좋다”는 의사를 알리는 역할
메모리 복사 X
자원 이동 X
내부 상태 변경 X
→ 진짜 “move” 작업은 move constructor / move assignment 안에서 직접 구현해야 한다
struct Buffer {
int* data;
std::size_t size;
Buffer(std::size_t n)
: data(new int[n]), size(n) {}
~Buffer() {
delete[] data;
}
// copy constructor
Buffer(const Buffer& other)
: data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
// move constructor
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 원본 비우기
other.size = 0;
}
};
int main() {
Buffer a(1000);
Buffer b = std::move(a); // 여기서 move ctor 호출
// std::move는 a를 rvalue로 캐스팅만 해줌
// 진짜 포인터 옮기기 / other 비우기는 Buffer(Buffer&&) 안에서 일어남
//이제 a는 moved-from 객체이다.
//moved-from 객체는 “valid but unspecified state” 여야 한다.
}
Scott Meyers가 Effective Modern C++에서 universal reference라고 부른 개념이고,
C++ 표준 용어로는 forwarding reference라고 부른다.
템플릿 파라미터 T에 대해 T&& 형태로,
그리고 T가 타입 추론(deduction)되는 위치에 있는 레퍼런스
함수 템플릿의 파라미터가 T&&이고,
호출 인자를 보고 T를 추론하는 그런 자리의 레퍼런스 → forwarding reference
ex) 대부분 클래스 템플릿은 vector<int.>식으로 타입이 명시되기 때문에(추론 x) T&&가 rvalue reference이다.
예:
template <typename T>
void f(T&& x); // 여기서 x는 forwarding reference (universal reference)
여기서 x는 상황에 따라 자동으로 lvalue reference 또는 rvalue reference가 된다.
void test() {
int a = 10;
const int ca = 20;
f(a); // T = int& -> 매개변수 타입: int& && -> int&
f(ca); // T = const int&-> 매개변수 타입: const int& && -> const int&
f(10); // T = int -> 매개변수 타입: int&&
}
lvalue를 넘기면 → T가 int&, const int& 이런 식으로 deduce
rvalue를 넘기면 → T가 그냥 int, std::string 같은 값 타입으로 deduce
그 다음 T&&에 reference collapsing 규칙이 적용됨.
C++에서는 &와 &&가 섞여서 겹칠 때(특히 템플릿에서) 어떤 reference 타입이 되는지에 대한 규칙이 필요하다.
규칙은 딱 하나 테이블로 기억하면 됨:
조합 ---> 결과
T& & ---> T&
T& && ---> T&
T&& & ---> T&
T&& && ---> T&&
즉,
“한 번이라도 &가 끼면 결국 &가 된다” & = 1, && = 0의 OR 연산이라고 생각하면된다.
(&& &&인 경우만 진짜 && 유지)
예를 들어:
template <typename T>
void f(T&& x);
int main() {
int i = 0;
f(i);
}
여기서:
i는 lvalue → T = int&
매개변수 타입: T&& → int& && → reference collapsing → int&
그래서 결국 x는 int&가 됨 (lvalue reference)
반면 rvalue인 f(0);은:
T = int
매개변수 타입 int&& && → int&&
진짜 rvalue reference로 받게 됨.
T&&가 상황에 따라 lvalue도, rvalue도 바인딩할 수 있어서 universal/forwarding reference라고 부르는 이유이다
문제 상황을 먼저 보자:
void foo(const std::string& s) { /* lvalue 전용 */ }
void foo(std::string&& s) { /* rvalue 전용 */ }
template <typename T>
void wrapper(T x) {
foo(x); // ??? 어떤 foo가 불릴까?
}
여기서 wrapper를 이렇게 쓰면:
std::string s = "hi";
wrapper(s); // foo(const std::string&) 기대
wrapper(std::string("hi")); // 원래는 foo(std::string&&) 기대
하지만 wrapper 안에서 x는 항상 lvalue이다. (x는 이름 있는 변수이기 때문에)
그래서 두 경우 모두 foo(const std::string&)이 호출된다.
“원래 인자가 lvalue면 lvalue로, rvalue면 rvalue로 그대로 전달하고 싶지만
wrapper 때문에 다 lvalue가 되어버림 → 이걸 해결하려는 기법이 perfect forwarding”
perfect forwarding을 위해 필요한 두 가지:
매개변수 타입: T&& (forwarding reference)
인자 전달: std::forward(arg)
template <typename T>
void wrapper(T&& x) {
foo(std::forward<T>(x));
}
개념적으로는:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& x) {
return static_cast<T&&>(x);
}
T가 lvalue reference인 경우 (T = U&):
std::forward(x) → static_cast<U&>(x) → lvalue로 캐스팅
T가 값 타입인 경우 (T = U):
std::forward(x) → static_cast<U&&>(x) → rvalue로 캐스팅
즉,
“T가 어떻게 deduce되었는지에 따라
x를 lvalue 또는 rvalue로 되돌려 보내는 캐스팅 함수”
그래서:
std::string s = "hi";
wrapper(s);
// T = std::string& → std::forward<T>(x) == lvalue → foo(const std::string&)
wrapper(std::string("hi"));
// T = std::string → std::forward<T>(x) == rvalue → foo(std::string&&)
→ 원래 인자의 value category(lvalue/rvalue)를 그대로 보존해서 forward하는 것
→ 그래서 perfect forwarding 이라고 부름.
가장 흔한 패턴:
template <typename F, typename... Args>
decltype(auto) call_with_log(F&& f, Args&&... args) {
std::cout << "calling...\n";
return std::forward<F>(f)(
std::forward<Args>(args)... // 각각의 l/rvalue 성질 그대로 전달
);
}
함수 객체 f와 인자 args...를 받아서
로깅을 찍고
원래대로 f(args...)를 호출하되,
각 인자가 lvalue인지 rvalue인지 그대로 유지해서 전달
std::function, 래퍼, 데코레이터, 이벤트 시스템, 커스텀 make_* 함수 등에서 많이 나오는 패턴이다
함수 하는 일 언제 쓰나
std::move(x) x를 무조건 rvalue(xvalue)로 캐스팅 “이 자원은 이제 이쪽으로 완전히 넘길게”
std::forward(x) T에 따라 lvalue 또는 rvalue로 조건부 캐스팅 템플릿에서 원래 인자의 value category 유지하여 전달
일반 코드(템플릿 아니거나, 항상 move하고 싶은 곳)에서는 → std::move
템플릿 래퍼, 팩토리, wrapper 함수에서 “원래 인자 상태 그대로 전달” 하고 싶으면 → std::forward + forwarding reference
template <typename T>
void my_swap(T& a, T& b) {
T temp(std::move(a)); // a의 자원 통째로 temp로 이동
a = std::move(b); // b의 자원을 a로 이동
b = std::move(temp); // temp의 자원을 b로 이동
}
여기서는 a, b, temp가 전부 정확히 어떤 객체를 가리키는지 명확하고
“무조건 자원을 이동해도 된다”는 의도가 분명하므로 → std::move가 맞다
template <typename T, typename... Args>
std::unique_ptr<T> make_my_unique(Args&&... args) {
return std::unique_ptr<T>(
new T(std::forward<Args>(args)...)
);
}
std::make_unique랑 같은 패턴
인자가 lvalue면 lvalue 그대로, rvalue면 rvalue 그대로 T 생성자에 전달
template <typename Func, typename... Args>
decltype(auto) call_with_retry(Func&& f, Args&&... args) {
for (int i = 0; i < 3; ++i) {
try {
return std::forward<Func>(f)(
std::forward<Args>(args)...
);
} catch (...) {
std::cout << "retry " << i << "\n";
}
}
throw std::runtime_error("failed after 3 retries");
}
f가 함수 포인터든, 람다든 상관없이 받아서
args... 역시 lvalue/rvalue 그대로 유지해서 넘길 수 있음
<결론>
T&& (forwarding reference) + reference collapsing 규칙 + std::forward 조합 덕분에,
템플릿 함수는 인자의 lvalue/rvalue 성질을 그대로 유지해서 다른 함수로 전달할 수 있다.만약 이 패턴이 없다면,
lvalue / rvalue / const / non-const 조합마다 별도의 오버로드를 전부 작성해야 해서
wrapper / decorator / factory / std::make_unique, emplace 같은 함수들은
사실상 구현 불가능에 가까운 수준으로 복잡해진다.