C++ 문법 정리 - 3 lvalue, rvalue, Move Semantics, Universal References, Perfect Forwading
문법 정리
lvalue, rvalue, references, 이동생성자, 이동연산자
lvalue는 주소값을 &
을 붙여 알아낼 수 있고 rvalue는 그렇지 않다.
대개 문법에서 lvalue가 왼쪽에 rvalue가 오른쪽에 놓이지만 꼭 그런것은 아니다.
&
하나만을 이용해 참조를 만드는 문법을 좌측값 레퍼런스(lvalue reference
)라고 한다.
하지만 const
가 붙어서 const T&
같은 인자는 rvalue도 참조로 받을 수 있다.
복사 생성자가 유의미하게 많이 호출되어 성능상 문제가 생기는 것을 방지하기 위해 이동연산이라는 것이 c++11 부터 언어적으로 지원되었다.
이는 rvalue references와 관련이 되어있다.
&&
처럼 사용된다. &&
는 rvalue references의 의미이다.
class MyString {
private:
char* buffer;
public:
// 이동 생성자
MyString(MyString&& other) noexcept : buffer(other.buffer) {
other.buffer = nullptr;
}
// 이동 대입 연산자
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] buffer;
buffer = other.buffer;
other.buffer = nullptr;
}
return *this;
}
// 기타 생성자와 대입 연산자 등...
};
move
함수
c++11부터 std::move
를 이용해 lvalue를 rvalue로 바꿀 수 있다.
C++ 의 원저자인 Bjarne Stroustroup 은 move 라고 이름을 지은 것을 후회했다고 합니다. 정확히 말하면 move 함수는 move 를 수행하지 않고 그냥 우측값으로 캐스팅만 하기 때문이죠! 더 적절한 이름은 rvalue 와 같은 것이 되겠습니다.
이는 다음과 같이 쓸 데 없이 복사가 3번 일어나는 swap
함수에서 성능 최적화를 할 수 있다.
template <typename T>
void my_swap(T &a, T &b) {
T tmp(a);
a = b;
b = tmp;
}
이제 std::move
를 쓴다면
template <typename T>
void my_swap(T &a, T &b) {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
만약 어떤 B안에 A객체가 멤버변수로 있고 그것에 대한 이동생성자를 B의 생성자에서 호출해주기 위해선 B의 생성자에서도 A의 객체를 rvalue로 받고 A를 생성해줄 때도 rvalue로 캐스팅해주어야 한다.
#include <iostream>
class A {
public:
A() { std::cout << "ctor\n"; }
A(const A& a) { std::cout << "copy ctor\n"; }
A(A&& a) { std::cout << "move ctor\n"; }
};
class B {
public:
B(A&& a) : a_(std::move(a)) {}
A a_;
};
int main() {
A a;
std::cout << "create B-- \n";
B b(std::move(a));
}
Universal References & Perfect Forwarding & Reference Collapsing Rule
&&
는 rvalue references를 의미한다고 하였지만 이것이 템플릿 인자에 사용되면 Universal References가 된다.
Universal References는 lvalue, rvalue와 관련없이 참조를 받을 수 있고, 그것을 std::forward
로 Perfect Forwarding을 써서 참조 타입을 정확히 유지하며 그대로 다른곳으로 넘길 수 있다.
여기서 std::move
를 쓰지 않는 이유는 lvalue에 std::move
를 쓰면 rvalue로 캐스팅이 되어버리기 때문인데, 따라서 lvalue는 lvalue로, rvalue는 rvalue로 참조 타입을 유지시켜주는 std::forward
가 등장했다.
template<typename T>
void func(T&& arg) {
someOtherFunc(std::forward<T>(arg));
}
다음은 씹어먹는 C++의 예시이다.
#include <iostream>
template <typename T>
void wrapper(T&& u) {
g(std::forward<T>(u));
}
class A {};
void g(A& a) { std::cout << "좌측값 레퍼런스 호출" << std::endl; }
void g(const A& a) { std::cout << "좌측값 상수 레퍼런스 호출" << std::endl; }
void g(A&& a) { std::cout << "우측값 레퍼런스 호출" << std::endl; }
int main() {
A a;
const A ca;
std::cout << "원본 --------" << std::endl;
g(a);
g(ca);
g(A());
std::cout << "Wrapper -----" << std::endl;
wrapper(a);
wrapper(ca);
wrapper(A());
}
원본 --------
좌측값 레퍼런스 호출
좌측값 상수 레퍼런스 호출
우측값 레퍼런스 호출
Wrapper -----
좌측값 레퍼런스 호출
좌측값 상수 레퍼런스 호출
우측값 레퍼런스 호출
Reference Collapsing rule은 레퍼런스 겹침 규칙으로, &
의 개수의 기우성에 따라 참조의 타입을 결정하는 방식이다.
&
가 홀수라면 A&
(lvalue reference), 짝수라면 A&&
(rvalue reference)가 된다.
Comments