
1차 업데이트: 2023.01.18
참조자의 특징
- 참조자는 표현식(expression)에서 암시적으로 역참조(dereference)되기 때문에 표현식의 타입은 절대로 참조자가 될 수 없다.
void Function(int& InValue) /* InValue의 타입은 int& */
{
auto X = InValue /* X의 타입은 int(int&가 아니다!) */
auto& Y = InValue /* Y의 타입은 int& */
}

- 참조자는 객체가 아니다. 따라서 참조자를 가르키는 포인터를 얻을 수 없고, 참조자로 이뤄진 배열은 정의할 수 없다.
- 참조자에는 연산자를 적용시킬 수 없다. 아래의 코드에서 ++는 ValueReference가 참조하는 int, 즉 Value에 적용된다.
int Value = 0;
int& ValueReference {Value};
++ValueReference; /* Value가 1로 증가된다 */
int* ValuePointer = &ValueReference /* ValuePointer는 Value를 가리킨다 */

포인터와 참조자의 공통점
- 객체(object)의 별칭(alias)이다.
- 객체의 기계 주소를 보관하기 위해 구현된다.
포인터와 참조자의 차이점
- 참조자는 객체의 이름을 사용할 때와 똑같은 문법으로 접근할 수 있다.
- 참조자는 항상 자신이 처음에 참조하게 초기화된 객체만을 객체를 참조한다.
- '널 참조자(null reference)'는 존재하지 않으며, 참조자는 무조건 어떤 객체를 참조하고 있다는 가정을 할 수 있다.
참조자의 종류
좌변 값/우변 값(lvalue/rvalue)와 상수/비상수(const/non-const) 구분을 반영하기 위해 3가지 종류의 참조자가 있다. 한 종류 이상의 참조자를 갖는 기본적인 의도는 객체를 다양하게 활용하도록 지원하는 것이다.
- 좌변 값 참조자(lvalue reference)
- non-const 좌변 값 참조자(non-const lvalue reference): 값을 변경하고 싶은 객체를 참조할 때
- const 좌변 값 참조자(const lvalue reference): 값을 변경하고 싶지 않은 객체(상수 등)를 참조할 때
- 우변 값 참조자(rvalue reference): 사용한 후에는 값을 보존할 필요가 없는 객체(임시 객체 등)를 참조할 때
non-const 좌변 값 참조자(non-const lvalue reference)
- T&에 대한 초기화 식은 T 타입의 좌변 값이어야 한다.
- 좌변 값 참조자를 함수의 인자로 지정하여 해당 함수로 전해진 객체의 값을 바꾸는 데 사용할 수 있다.(프로그램의 가독성을 높이려면 대다수의 경우에는 자신의 인자를 변경하는 함수는 피하고 명시적으로 값을 반환하는 편이 좋다.)
/* InValue는 Value의 또 다른 이름이 된다 */
void Increment(int& InValue) { ++a; }
void Function()
{
int Value = 1;
Increment(Value); /* Value = 2 */
}
- 참조자는 아래 코드에서와 같이 반환 타입으로 사용될 수도 있다. 이런 방식은 대입 연산(assignment)의 좌변과 우변에서 모두 활용될 수 있는 함수를 정의하는 데 주로 사용된다. 여기서는 키 인자 InKey를 참조자로 전달하는데, 해당 인자가 복사하기에는 비싼 타입일 수 있기 때문이다. 마찬가지 이유로 값 또한 참조자로 반환한다. 또한 InKey에 대해서는 const 참조자(4-2)를 사용하는데, 이는 해당 인자를 수정하고 싶지 않을 뿐만 아니라 인자로 리터럴이나 임시 객체를 사용하고 싶기 때문이다. 반대로 반환된 결과는 사용자가 수정할 가능성이 높기 때문에 non-const 참조자로 반환한다.
template<typename Key, typename Value>
Value& Map<Key, Value>::operator[] (const Key& InKey)
{
for (auto& Element : Elements)
{
if (InKey == Element.first) { return Element.second; }
}
Elements.push_back({InKey, Value{}});
return Elements.back().second;
}
const 좌변 값 참조자(const lvalue reference)
- const T&에 대한 초기화 식은 좌변 값일 필요가 없으며, 심지어 T 타입이 아니어도 된다. 이러한 경우에는
- 우선 필요한 경우 T 타입으로의 암시적 타입 변환이 적용된다.
- 그런 다음, 그 결과 값이 타입 T의 임시 변수에 저장된다.
- 마지막으로 그 임시 변수가 초기화 식으로 사용된다.
double& DoubleReference = 1; /* Error: 좌변 값이 필요하다 */
const double& ConstDoubleReference {1}; /* OK */
위의 마지막 초기화를 해석하면 다음과 같다.
double Temp = double{1}; /* 먼저 우변 값으로 임시 변수를 생성하고 */
const double& ConstDoubleReference {Temp}; /* 해당 임시 변수를 초기화 식으로 활용한다 */
참조자의 초기화 식을 보관하기 위해 생성된 임시 객체는 해당 참조자의 유효 범위가 끝날 때까지 존속된다.
- 변수에 대한 참조자와 상수에 대한 참조자가 구분되는 이유는 변수에 대한 참조자에서 상수에 대한 참조자와 같이 변수에 대한 임시 객체를 활용하면 오류가 발생할 가능성이 높기 때문인데, 해당 변수에 어떤 값을 대입하는 일은 곧 사라질 임시 변수에 대한 대입이 될 것이기 때문이다. 상수에 대한 참조자의 경우엔 이런 문제가 없으며, 함수 인자로써 자주 중요하게 사용된다.
우변 값 참조자(rvalue reference)
- 우변 값 참조자는 우변 값에는 묶일 수 있지만, 좌변 값에는 묶일 수 없다. 그런 점에서 우변 값 참조자는 좌변 값 참조자와 정확히 반대된다.
string Value {"AengChoon"};
string Function();
/* non-const 좌변 값 참조자 */
string& LValueRef1 {Value}; /* LValueRef1은 Value를 참조 */
string& LValueRef2 {Function()}; /* Error: Function()의 반환 값은 우변 값 */
string& LValueRef3 {"Game"}; /* Error: 임시 객체를 참조할 수 없다 */
/* const 좌변 값 참조자 */
const string& CLValueRef {"Client"}; /* CLValueRef는 만들어진 임시 객체를 참조 */
/* 우변 값 참조자 */
string&& RValueRef1 {Value}; /* Error: Value는 좌변 값 */
string&& RValueRef2 {Function()}; /* RValueRef2는 Function()의 반환 값(임시 객체)를 참조 */
string&& RValueRef3 {"Programmer"}; /* RValueRef3는 "Programmer"를 보관하는 임시 객체를 참조 */
- 임시 객체를 참조하면 때때로 값비싼 복사 처리를 저렴한 이동 처리로 바꿀 수 있다. 대용량의 가능성이 있는 객체는 그 원본이 다시 사용되지 않으리라는 점을 알기만 한다면 간단하고 저렴하게 이동될 수 있다.
- 우변 값 참조자를 사용하는 이점 대부분은 참조하는 객체에 값을 쓸 수 있는 것에서 기인하기 때문에 const 우변 값 참조자는 잘 사용되지 않는다.
- const 좌변 값 참조자와 우변 값 참조자는 모두 우변 값에 묶을 수 있지만 목적은 근본적으로 다르다.
- const 좌변 값 참조자는 참조하는 객체의 수정을 방지하기 위해 사용된다.
- 우변 값 참조자는 비싼 복사 대신 "소멸적 읽기(destructive read)"를 사용하여 최적화를 위해 사용된다.
- 우변 값 참조자에 의해 참조되는 객체는 좌변 값 참조자나 통상적인 변수 이름에 의해 참조되는 개체와 똑같은 방식으로 접근할 수 있다.
우변 값 참조자의 활용
template<typename T>
void Swap(T& A, T& B)
{
T Temp {A}; /* 타입 T의 복사 생성자 사용 */
A = B; /* 타입 T의 복사 대입 연산자 사용 */
B = Temp; /* 타입 T의 복사 대입 연산자 사용 */
}
위의 코드에서 T가 string이나 vector 같이 복사하기에는 값비싼 타입이라면 이러한 Swap()은 값비싼 처리가 된다. 우리에겐 복사가 전혀 필요하지 않고 단지 A, B, Temp의 값을 이동하고 싶을 뿐이다.
template<typename T>
void Swap(T& A, T& B)
{
T Temp {static_cast<T&&>(A)};
A = static_cast<T&&>(B);
B = static_cast<T&&>(A);
}
static_cast<T&&>(X)의 결과 값은 X에 대한 T&& 타입의 우변 값이다. 타입 T가 이동 생성자나 이동 대입 연산자를 가지고 있다면 우변 값에 최적화된 처리를 활용할 수 있다.
static_cast를 활용한 방식은 다소 장황한 데다 오타가 나기 쉬우므로 표준 라이브러리에서 move() 함수를 제공한다. move(X)는 타입이 T인 X의 static_cast<T&&>(X)를 의미한다. 이를 활용해서 Swap()을 정리할 수 있다.
template<typename T>
void Swap(T& A, T& B)
{
T Temp {std::move(A)};
A = std::move(B);
B = std::move(Temp);
}
타입 T가 이동 생성자나 이동 대입 연산자를 가지고 있다면 원래의 Swap()과 대조적으로 위의 버전은 아무런 복사본도 만들지 않는다. 가능한 곳에서는 모두 이동을 활용하는 것이다.
현재 Swap()은 좌변 값만을 인자로 받고 있기 때문에 아래와 같이 두 개의 오버로딩으로 우변 값을 인자로 받을 수 있게 함수를 보완할 수 있다.
template<typename T> void Swap(T&& A, T& B);
template<typename T> void Swap(T& A, T&& B);'C++ > Basics' 카테고리의 다른 글
| <C++ Basics> 이동(Move) (0) | 2023.06.02 |
|---|---|
| <C++ Basics> 복사(Copy) (0) | 2023.03.17 |
| <C++ Basics> 생성자(Constructor) (0) | 2023.02.28 |