모두의 코드 저자인 이재범 (Psi) 님께 감사하다는 인사를 먼저 드리고 시작한다. 지극히 개인적으로 필요한 내용만 발췌해서 정리하고, 여러 자료를 찾아서 보충한 페이지다.

Namesapce

Reference

int& ref = number;
void changeNumber(int& ref) {
    ref = 5; // change
}
void changeNumberPtr(int * ref) {
    *ref = 5; // change
}

두 함수가 하는 일은 똑같다. 하지만, 포인터 인자와 레퍼런스 인자의 성격은 비슷해 보이지만 다르다. 포인터 인자는 ‘주소’ 자체를 가져오는 행위고, 레퍼런스 인자는 ‘참조’ 라는 논리적 별칭을 가져오는 행위다.

따라서, 포인터 자체에 대한 수정이나 메모리 영역 너머를 반환해야 하는 경우는 여전히 포인터 인자를 써야 한다. 그런데, 이런 조작은 유지 보수를 망치는 지름길이다. 함수 안에서는 정해진 범위 안에서만 움직여야 한다는 것이다. C는 이게 자유로웠고, C++ 은 제약할 수 있는 여러 장치가 있는 것이다. 그러면서 효율성을 같이 추구한 것이 레퍼런스다.

함수 레퍼런스 인자의 실수

atomic 을 먼저 보고 있을 때, compare_exchange_strong() 의 함수 원형이 T& expected, T desired 여서 첫 번째 인자를 이상하게 줘봤지만 전부 컴파일 에러가 났었다.

atomic<bool> sLock;
bool sExpected = false;
// 에러 1
sLock.compare_exchange_strong(&sExpected, true);
// 에러 2
sLock.compare_exchange_strong(ref(sExpected), true);
// 성공 (...)
sLock.compare_exchange_strong(sExpected, true);

내 무지를 용서하기엔 너무 처참했다.

옆길로 새기 : reference_wrapper

아주 중요한 차이가 있다. 다음 코드를 보자.

    int a = 5;
    int b = 10;

    // reference
    int& reference = a;
    reference = b;

    cout << reference << endl;
    cout << "A is " << a << " and B is " << b << endl;
A is 10 and B is 10

레퍼런스를 ‘포인터’와 같은 개념으로 생각하면 이런 사단이 날 가능성이 높다. 왜 이렇게 됐냐면, reference 는 A를 가리키고 있지만 엄밀히 말하면 A의 별칭으로 선언된 것이다. 그래서 reference = b 에서 아예 A 메모리 영역에 B의 값을 할당했기 때문이다. 즉, A의 값이 같이 오염된다.

    // reference_wrapper
    auto reference = std::ref(a);
    reference = std::ref(b);

이 줄만 바꾸면, A는 오염되지 않는다.

A is 5 and B is 10

디버거로 reference 변수를 까보면, std:reference_wrapper<int> 형으로 나온다. 멤버로 포인터와 raw 값을 가지고 있기 때문에, 앞서 이야기한 reference = std::ref(b) 역시 b의 포인터와 raw 값을 가진 reference_wrapper 객체가 할당되는 것임을 알 수 있다. A의 값 자체는 보존된다는 뜻이다.

반환형 레퍼런스

다시 돌아와서, 이런 것도 있으니 눈여겨보자.

int &func2(int &a)

이렇게 함수 이름 앞에도 & 를 쓸 수 있다. 그럼 함수의 반환값 자체가 레퍼런스 (=별칭)잉 되는 것이다.

int &myFunc(int &a) { return a; }
int main(void)
{
    int sFirst = 5;
    cout << myFunc(sFirst)++ << endl;
    cout << sFirst << endl;
5
6

이런 구문이 실제 변수에 적용이 된단 소리다. myFunc() 의 반환값이 여전히 sFirst 를 가리키는 별칭이므로, 뒤의 incremental operator 가 작동할 수 있다.

myFunc 앞에 & 만 지워버리면 식이 수정할 수 있는 lvalue여야 합니다. 라는 에러가 발생한다. lvalue 는 저녁에 정리할 기회가 있을 것이다.

심화 : 레퍼런스는 메모리에 있다?

책도, 사이트도 전부 대답은 ‘아니오’ 이다. 레퍼런스 선언이 이뤄지면 포인터 변수처럼 변수로 자리하는게 아니다. 그래서 레퍼런스는 변수라고 하지 않는다.

레퍼런스 자체는 그저 특정 변수나 객체를 가리켜 둔 코드 레벨의 영역이다. 스택이나 힙과 관련이 없을 것으로 본다. (확인이 필요) 그런데 레퍼런스의 주기라고 하는 것은, 그게 가리키는 특정 변수나 객체의 생명 주기와 같이 한다고 생각하는 게 가장 짧은 설명이 될 것이다.

더 짧게 하면, 레퍼런스는 생명주기 자체를 논하는게 부적절하고, 그게 가리키는 변수의 생명주기만 보면 된다는 것이다.

예를 들어, 아까 레퍼런스 반환을 하던 함수 myFunc 을 생각해 보자. 이 함수가 끝나면 함수에서 쓰던 스택은 정리된다. 그런데 반환된 레퍼런스는 정리되면 안 된다. 바깥에서 (예제에서는 main 에서) 사용하고 있기 때문이다. 여기서 sFirst 는 지역 변수이니, main 이 끝나면 myFunc 이 반환한 레퍼런스도 그제야 끝나는 것이다.

그런데 좀 이상한 것은 만약 myFunc 의 반환 레퍼런스가 고정이라면 여러 함수 호출에 대한 대응은 못 한다는 소리로 들린다. myFunc(sFirst), myFunc(sSecond) 둘 다 하면? 레퍼런스는 2개가 생기는데? 그래서 C++11 은 이런 이유로 규정 자체를 하지 않고 있단다. (즉, 언어의 영역이 아닌 컴파일러와 OS의 책임이 됨)

It is unspecified whether or not a reference requires storage (3.7).

여담인데, 그렇기 때문에 reference_wrapper 는 객체이며, 이를 담아내는 선언은 객체 선언이 맞다. 스택이건 힙이건 메모리에 존재한다는 것이다.

좀 더 추가하자면, 이건 안 된다.

int getInt() { return 1; }
int main()
{
    int& e = getInt();
}

getInt() 의 반환은 값이기 때문에, 이게 임시변수에 잠시 저장되는 상태에서 레퍼런스 하는 것은 용납하지 않는다. 에러 메시지는 단순히 int 를 int& 로 바꿀 수 없습니다 라고 뜰 것이다.