C++에서는 rvalue 참조(&&)와 이동연산자(move constructor 및 move assignment operator)를 통해 효율적인 메모리 관리를 할 수 있습니다. 이 글에서는 이러한 개념들을 자세히 설명하고, 이를 통해 성능 최적화 방법을 이해할 수 있도록 하겠습니다.
rvalue 참조 (&&)
rvalue 참조는 C++11에서 도입된 기능으로, 주로 이동생성자(move constructor)와 이동할당 연산자(move assignment operator)를 구현하는 데 사용됩니다.
- lvalue와 rvalue
- lvalue는 식별 가능한 위치를 가지는 값입니다. 예를 들어, 변수는 lvalue입니다.
- rvalue는 임시 값으로, 식별 가능한 위치를 가지지 않는 값입니다. 예를 들어, 리터럴이나 연산의 결과는 rvalue입니다.
- rvalue 참조 (&&)
- rvalue 참조는 &&를 사용하여 선언되며, rvalue를 참조할 수 있습니다. lvalue를 참조하는 것처럼 rvalue를 참조한다고 생각하면 쉽게 이해할 수 있습니다.
- 주로 이동생성자와 이동할당 연산자에서 사용됩니다.
#include <iostream>
#include <utility> // std::move를 사용하기 위해 필요
void test(int&& x)
{
x = 10;
std::cout << x;
}
void test2(int x)
{
x = 10;
std::cout << x;
}
int main()
{
test(30); // test 함수 호출: rvalue 참조로 30 전달
test2(20); // test2 함수 호출: 값 타입으로 20 전달
return 0;
}
- 매개변수 타입:
- test 함수는 rvalue 참조 (int&&)를 매개변수로 받습니다.
- test2 함수는 일반적인 값 타입 (int)을 매개변수로 받습니다.
- 값 전달 방식:
- test 함수는 rvalue 참조를 통해 값을 전달받습니다. 이는 임시 객체나 이동 가능한 객체를 전달받을 때 주로 사용됩니다.
- test2 함수는 값을 복사하여 전달받습니다. 이는 호출 시 원본 값을 복사하여 함수 내부에서 사용합니다.
- 메모리 사용 방식:
- test 함수는 rvalue 참조를 사용하여 기존의 임시 객체나 이동 가능한 객체를 참조합니다. 따라서 복사가 일어나지 않습니다.
- test2 함수는 값을 복사하여 매개변수로 전달받습니다. 따라서 호출 시 매개변수로 전달되는 값의 복사가 일어납니다.
결과적으로 두 함수 모두 10을 출력하지만, 값 전달 방식과 메모리 사용 방식에서 차이가 있습니다. test 함수는 rvalue 참조를 사용하여 효율적으로 값을 전달하고 변경하는 반면, test2 함수는 값을 복사하여 전달하고 변경합니다.
std::move()에 대해
#include <iostream>
#include <utility> // std::move를 사용하기 위해 필요
void test(int&& x) {
x = 10;
std::cout << x << std::endl; // 변경된 x 출력
}
int main() {
int i = 50;
test(std::move(i)); // i를 rvalue로 변환하여 전달
std::cout << i << std::endl; // 변경된 i 출력
return 0;
}
std::move란?
- std::move()란 객체를 강제로 rvalue로 캐스팅하여 이동을 사용할 수 있게 하는 유틸리티입니다. 이를 통해 복사 대신 이동을 수행하여 자원 소모를 줄이고 성능을 최적화할 수 있습니다.
- std::move()는 lvalue의 값을 rvalue처럼 취급하도록 해줍니다.
std::move를 통한 lvalue -> rvalue 변환:
- std::move(i)는 변수 i를 rvalue로 캐스팅합니다. i는 원래 lvalue(좌값)입니다.
- std::move는 단순히 i를 rvalue로 취급하도록 캐스팅할 뿐, 실제로 데이터를 이동시키는 것은 아닙니다.
rvalue reference
- test(int&& x) 함수는 rvalue reference를 인자로 받습니다. rvalue reference는 임시 객체나 리소스의 소유권을 넘길 때 주로 사용됩니다.
함수 호출
- test(std::move(i))를 호출하면, i의 값(50)이 x로 전달됩니다. 이때 x는 rvalue reference로 i를 가리키게 됩니다.
- 함수 내부에서 x = 10을 통해 x의 값을 10으로 변경합니다. 이 변경은 실제로 i에 영향을 미칩니다.
출력
- 함수 test 내부에서 std::cout << x는 10을 출력합니다.
- main 함수에서 std::cout << i는 10을 출력합니다. 이는 i의 값이 test 함수 호출 중에 변경되었기 때문입니다.
우측 값 참조의 연산자와 생성자의 동작 원리
int main()
{
int&& rref = 5; // 'rref'는 임시 객체 '5'를 참조하는 rvalue 참조
int b = 10;
rref = std::move(b); // 'b'를 rvalue로 캐스팅하여 'rref'에 이동
b = 3;
cout << b << " " << rref << endl; //3 10
rref = 7;
cout << b << " " << rref; //3 7
return 0;
}
첫 번째 예제 (대입 연산자): rref는 임시 객체 5를 참조하도록 생성되었습니다. rref = std::move(b)는 rref가 b의 값을 받도록 하지만, rref와 b는 메모리 주소를 공유하지 않습니다. 따라서 b와 rref는 서로 독립적인 값을 가집니다.
int main()
{
int a = 5; // 'a'는 lvalue
int& lref = a; // 'lref'는 'a'를 참조하는 lvalue 참조
int b = 10;
int&& rref = std::move(b); // 'rref'는 b를 참조하는 rvalue 참조
rref = 7;
cout << b << " " << rref << endl; // 출력: 7 7
b = 3;
cout << b << " " << rref << endl; // 출력: 3 3
return 0;
}
두 번째 예제 (생성자) : rref는 처음부터 std::move(b)를 통해 b를 참조하도록 생성되었습니다. 이로 인해 rref와 b는 동일한 메모리 주소를 공유하게 됩니다. 따라서 rref의 값을 변경하면 b의 값도 변경됩니다.
Move Semantics
이동(Move Semantics)은 C++11에서 도입된 기능으로, 객체의 복사 대신 이동을 통해 성능을 최적화할 수 있도록 해줍니다. 이동은 주로 자원의 소유권을 이전할 때 사용되며, 복사보다 훨씬 효율적입니다. 이를 통해 불필요한 복사 연산을 줄이고, 프로그램의 성능을 향상시킬 수 있습니다. 아래의 내용은 Move Semantics를 확인할 수 있는 좋은 예제입니다.
move()를 사용하여 이동할당
#include <iostream>
#include <utility> // std::move를 사용하기 위해 필요
class Resource {
public:
int* data;
// 기본 생성자
Resource() : data(new int[10]) {
std::cout << "Resource acquired\n";
}
// 소멸자
~Resource() {
delete[] data;
std::cout << "Resource destroyed\n";
}
// 복사 생성자
Resource(const Resource& other) : data(new int[10]) {
std::copy(other.data, other.data + 10, data);
std::cout << "Resource copied\n";
}
// 이동생성자
Resource(Resource&& other) noexcept : data(nullptr) {
data = other.data;
other.data = nullptr;
std::cout << "Resource moved\n";
}
// 이동할당 연산자
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
std::cout << "Resource moved via assignment\n";
}
return *this;
}
// 복사할당 연산자(필요시 구현)
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
data = new int[10];
std::copy(other.data, other.data + 10, data);
std::cout << "Resource copied via assignment\n";
}
return *this;
}
};
int main() {
Resource res1; // 기본 생성자를 통해 자원 할당
Resource res2 = res1; // 복사 생성자를 통해 자원 복사
Resource res3; // 기본 생성자를 통해 자원 할당
res3 = res1; // 복사할당 연산자를 통해 자원 복사
Resource res4 = std::move(res1); // 이동생성자를 통해 자원 이동
Resource res5; // 기본 생성자를 통해 자원 할당
res5 = std::move(res4); // 이동할당 연산자를 통해 자원 이동
return 0;
}
각 연산자, 생성자의 설명
- 기본 생성자는 Resource 객체를 생성하고 data 포인터에 동적으로 할당된 int 배열을 초기화합니다. "Resource acquired" 메시지를 출력합니다.
- 복사 생성자는 res1을 res2로 복사하여 새로운 Resource 객체를 생성합니다. 복사 생성자는 res1의 데이터를 복사하여 res2에 새로운 int 배열을 할당하고 데이터를 복사합니다. "Resource copied" 메시지를 출력합니다.
- 복사 할당 연산자는 이미 생성된 res3 객체에 res1의 데이터를 복사합니다. 복사할당 연산자는 res1의 데이터를 res3의 data에 복사합니다. "Resource copied via assignment" 메시지를 출력합니다.
- 이동 생성자는 res1의 자원을 res4로 이동시켜 새로운 Resource 객체를 생성합니다. 이동 생성자는 res1의 data 포인터를 res4로 이동시키고, res1의 data 포인터를 nullptr로 설정합니다. "Resource moved" 메시지를 출력합니다.
- 이동 할당 연산자는 이미 생성된 res5 객체에 res4의 자원을 이동시킵니다. 이동할당 연산자는 res4의 data 포인터를 res5로 이동시키고, res4의 data 포인터를 nullptr로 설정합니다. "Resource moved via assignment" 메시지를 출력합니다.
정리
rvalue 참조 (&&)
- rvalue는 우측값의 참조입니다. 해당 값을 참조하여 생성된 변수는 원본 메모리를 참조하므로, 값이 변경될 때 참조된 곳과 함께 변경됩니다.
- 생성 시 rvalue 참조를 사용하면, 원본 값을 직접 참조하므로 효율적인 메모리 사용이 가능합니다.
- 반면, rvalue 참조를 연산자에서 대입할 경우에는 메모리 주소를 공유하지 않으므로 서로의 값 변경에 영향을 미치지 않습니다.
std::move()
- std::move()는 객체를 강제로 rvalue로 캐스팅하여 이동을 사용할 수 있게 합니다.
- 이동 생성자나 이동 할당 연산자를 통해 기존 객체의 메모리 주소를 새로운 객체에 대입하고, 기존 객체의 자원을 해제하여 이전 객체의 자원을 비워줍니다. 이를 통해 메모리 할당 및 해제를 최소화하고 효율적으로 리소스를 이전할 수 있습니다.
- 또한, 우측값 참조에 값을 넣어줄 때 std::move()를 사용합니다.
그 외의 추가 정보
- 이동을 지원할 객체는 const로 선언하지 말아야한다. const 객체에 대한 이동 요청은 복사 연산으로 변환된다.
- ...
보편 참조(Universal Reference)
보편참조는 C++11에서 도입된 개념으로, 함수 템플릿의 매개변수에서 사용됩니다. 이는 T&& 형식의 참조를 의미하며, lvalue와 rvalue를 모두 참조할 수 있는 참조 유형입니다. 보편 참조는 템플릿 타입 유추와 결합되어 동작합니다.
#include <iostream>
#include <utility> // std::forward를 사용하기 위해 필요
template<typename T>
void func(T&& arg) {
handle(std::forward<T>(arg));
}
void handle(int& x) {
std::cout << "Lvalue reference: " << x << std::endl;
}
void handle(int&& x) {
std::cout << "Rvalue reference: " << x << std::endl;
}
int main() {
int a = 10;
func(a); // Lvalue 전달
func(20); // Rvalue 전달
return 0;
}
- 템플릿 함수 func: T&& 형식의 매개변수 arg를 받습니다. 이는 보편 참조입니다.
- std::forward<T>(arg): 템플릿 인자를 그대로 전달하여 적절한 함수 오버로드가 선택되도록 합니다. lvalue는 lvalue로, rvalue는 rvalue로 전달됩니다.
- handle 함수: lvalue 참조와 rvalue 참조를 각각 처리하는 오버로드된 함수입니다.
위 예제에서 func(a)는 lvalue 참조를 전달하며, func(20)는 rvalue 참조를 전달합니다. 보편 참조를 사용함으로써 func 함수는 lvalue와 rvalue 모두를 유연하게 처리할 수 있습니다.
참고사이트
'C++ > Effective Modern' 카테고리의 다른 글
초기화 (1) | 2023.12.06 |
---|---|
[C++] 형식 연역 규칙 (0) | 2022.07.05 |