C++에서 함수 매개변수를 전달하는 방식은 크게 값(Value) 타입과 참조(Reference) 타입으로 나뉩니다. 참조 타입은 다시 C++11에서 도입된 우측값 참조와 기존의 좌측값 참조로 구분됩니다. 각 방식의 특성과 메모리 동작 방식을 이해하는 것은 고성능 C++ 코드를 작성하는 데 필수적입니다.
좌측값(Lvalue)과 우측값(Rvalue)의 정의
매개변수 전달 방식을 이해하기 전, 좌측값과 우측값의 차이를 명확히 해야 합니다.
- 좌측값(Lvalue): 메모리상의 고유한 주소를 가지며, 표현식의 왼쪽과 오른쪽 모두에 위치할 수 있는 객체입니다. 이름이 있는 변수가 대표적인 예입니다.
- 우측값(Rvalue): 표현식 내에서만 존재하는 임시 객체로, 지속적인 메모리 주소를 갖지 않습니다. 일반적으로 대입 연산자의 오른쪽에만 나타납니다.
int x = 100; // x는 좌측값, 100은 우측값
값 타입(Pass-by-Value) 매개변수
값 타입은 가장 기본적인 전달 방식입니다. 함수 호출 시 다음과 같은 동작이 발생합니다.
- 좌측값 전달 시: 객체의 복사본이 생성됩니다(복사 생성자 호출). 함수 내부에서의 변경은 원본 객체에 영향을 주지 않습니다.
- 우측값(또는 std::move) 전달 시: 이동 생성자가 호출됩니다. 이는 불필요한 자원 할당을 방지하여 성능을 최적화합니다.
객체의 생명주기를 관찰하기 위해 다음과 같은 DataBuffer 클래스를 정의하여 테스트해 보겠습니다.
#include <iostream>
#include <utility>
class DataBuffer {
public:
explicit DataBuffer(int id) : id_(id) {
std::cout << "생성자 호출: " << id_ << " (" << this << ")" << std::endl;
}
DataBuffer(const DataBuffer& other) : id_(other.id_) {
std::cout << "복사 생성자: " << id_ << " (" << this << " <- " << &other << ")" << std::endl;
}
DataBuffer(DataBuffer&& other) noexcept : id_(other.id_) {
std::cout << "이동 생성자: " << id_ << " (" << this << " <- " << &other << ")" << std::endl;
other.id_ = 0;
}
~DataBuffer() {
std::cout << "소멸자 호출: " << id_ << " (" << this << ")" << std::endl;
}
private:
int id_;
};
void processByValue(DataBuffer buffer) {
std::cout << "값 전달 함수 내부 실행" << std::endl;
}
값 타입 매개변수를 가진 함수에 좌측값과 우측값을 전달했을 때의 차이는 다음과 같습니다.
int main() {
DataBuffer obj(10);
std::cout << "--- 좌측값 전달 시작 ---" << std::endl;
processByValue(obj); // 복사 생성자 호출
std::cout << "--- 우측값 전달 시작 ---" << std::endl;
processByValue(std::move(obj)); // 이동 생성자 호출
return 0;
}
좌측값 참조(Lvalue Reference)
좌측값 참조는 & 기호를 사용하며, 기존 객체에 대한 별칭을 생성합니다. 이는 대규모 객체의 복사 비용을 절약하는 데 유용합니다.
1. 비상수 좌측값 참조 (T&)
함수 내부에서 원본 데이터를 수정해야 할 때 사용합니다. 오직 수정 가능한 좌측값만 바인딩할 수 있습니다.
void modifyBuffer(DataBuffer& ref) {
std::cout << "참조 전달 실행: " << &ref << std::endl;
}
int main() {
DataBuffer obj(20);
modifyBuffer(obj); // 가능
// modifyBuffer(DataBuffer(30)); // 컴파일 에러: 우측값을 비상수 좌측값 참조에 바인딩 불가
return 0;
}
2. 상수 좌측값 참조 (const T&)
원본 데이터를 보호하면서 복사 비용을 피하고 싶을 때 사용합니다. 상수 좌측값 참조는 좌측값뿐만 아니라 우측값도 바인딩할 수 있다는 중요한 특징이 있습니다.
void readBuffer(const DataBuffer& ref) {
std::cout << "상수 참조 실행: " << &ref << std::endl;
}
int main() {
DataBuffer obj(40);
readBuffer(obj); // 좌측값 가능
readBuffer(std::move(obj)); // 우측값 가능
return 0;
}
참고로, std::move를 사용하여 상수 좌측값 참조에 전달하더라도 이동 생성자는 호출되지 않습니다. std::move는 단순히 타입을 우측값 참조로 캐스팅할 뿐, 실제 자원 이동을 수행하는 것은 아니기 때문입니다.
우측값 참조(Rvalue Reference)
C++11에서 도입된 && 기호를 사용하며, 소유권 이전(Move Semantics)을 명시적으로 처리하기 위해 사용됩니다. 오직 우측값만 매개변수로 받을 수 있습니다.
void moveBuffer(DataBuffer&& rref) {
std::cout << "우측값 참조 함수 호출: " << &rref << std::endl;
}
int main() {
DataBuffer obj(50);
// moveBuffer(obj); // 에러: 좌측값 바인딩 불가
moveBuffer(std::move(obj)); // 성공: std::move를 통해 우측값으로 변환
return 0;
}
요약 및 가이드라인
- 값 타입: 이동 생성자를 통해 소유권을 완전히 이전하거나, 함수 내부에서 복사본이 필요한 경우에 적합합니다.
- 좌측값 참조: 원본 객체를 효율적으로 참조하거나 수정해야 할 때 사용합니다. 특히
const T&는 읽기 전용 매개변수의 표준적인 방식입니다. - 우측값 참조: 이동 전용 타입(예:
std::unique_ptr)을 처리하거나, 자원 재사용을 통한 성능 최적화가 필요할 때 사용합니다. - 이동 생성자를 실행시키고 싶다면, 함수 매개변수를 값 타입으로 설계하고 인자로 우측값을 전달하거나, 함수 내부에서 우측값 참조 매개변수를 활용하여 자원을 처리해야 합니다.