참조

참조 (Reference)

참조는 이미 존재하는 객체나 함수의 별칭(alias) 역할을 하는 이름 있는 변수예요. 값으로 복사하는 대신 원본을 그대로 다루고 싶을 때, 특히 함수 인자나 반환 값으로 쓸 때 유용해요. 이 페이지에서는 참조 선언 문법, 참조 축약(reference collapsing), lvalue·rvalue·전달 참조까지 설명할게요.

출처: cppreference

본문

참조 선언은 declarator가 다음 형태를 가진 어떤 simple-declaration이에요.

& attr (optional) declarator      (1)
&& attr (optional) declarator     (2) (since C++11)
  1. lvalue 참조 declarator: 선언 S& D;D를 decl-specifier-seq S가 정하는 타입에 대한 lvalue 참조로 선언해요.
  2. rvalue 참조 declarator: 선언 S&& D;DS가 정하는 타입에 대한 rvalue 참조로 선언해요.

참조는 유효한 객체나 함수를 가리키도록 초기화되어야 해요: 참조 초기화(reference initialization) 참고.

"(cv-qualified) void에 대한 참조" 타입은 만들 수 없어요.

참조 타입은 최상위에서 cv-qualified될 수 없어요. 선언에 그런 문법이 없고, typedef-name이나 decltype 지정자(since C++11), 타입 템플릿 매개변수에 qualification이 추가되면 무시돼요.

참조는 객체가 아니에요. 반드시 저장 공간을 차지할 필요는 없지만, 컴파일러가 원하는 의미론을 구현하기 위해 필요하면 저장 공간을 할당할 수도 있어요(예: 참조 타입의 비정적 데이터 멤버는 보통 메모리 주소를 저장할 만큼 클래스 크기를 키워요).

참조는 객체가 아니므로 참조의 배열, 참조에 대한 포인터, 참조에 대한 참조는 없어요.

int& a[3]; // error
int&* p;   // error
int& &r;   // error

참조 축약 (Reference collapsing)

템플릿이나 typedef의 타입 조작을 통해 참조에 대한 참조를 형성하는 것이 허용되는데, 그 경우 참조 축약 규칙이 적용돼요. rvalue 참조에 대한 rvalue 참조는 rvalue 참조로 축약되고, 그 외 모든 조합은 lvalue 참조가 돼요.

typedef int&  lref;
typedef int&& rref;
int n;

lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

(이것은 함수 템플릿에서 T&&를 쓸 때의 템플릿 인자 추론 특별 규칙과 함께, std::forward를 가능하게 하는 규칙을 이뤄요.) (since C++11)

lvalue 참조 (Lvalue references)

lvalue 참조는 기존 객체를 별칭하는 데 쓸 수 있어요(선택적으로 다른 cv-qualification으로).

#include <iostream>
#include <string>

int main()
{
    std::string s = "Ex";
    std::string& r1 = s;
    const std::string& r2 = s;

    r1 += "ample";           // modifies s
//  r2 += "!";               // error: cannot modify through reference to const
    std::cout << r2 << '\n'; // prints s, which now holds "Example"
}

함수 호출에서 값 전달(pass-by-reference) 의미론을 구현하는 데에도 쓸 수 있어요.

#include <iostream>
#include <string>

void double_string(std::string& s)
{
    s += s; // 's' is the same object as main()'s 'str'
}

int main()
{
    std::string str = "Test";
    double_string(str);
    std::cout << str << '\n';
}

함수의 반환 타입이 lvalue 참조이면, 함수 호출 표현식은 lvalue 표현식이 돼요.

#include <iostream>
#include <string>

char& char_number(std::string& s, std::size_t n)
{
    return s.at(n); // string::at() returns a reference to char
}

int main()
{
    std::string str = "Test";
    char_number(str, 1) = 'a'; // the function call is lvalue, can be assigned to
    std::cout << str << '\n';
}

rvalue 참조 (Rvalue references)

rvalue 참조는 임시 객체의 수명을 확장하는 데 쓸 수 있어요(const에 대한 lvalue 참조도 임시 객체의 수명을 확장하지만, 그것을 통해 수정할 수는 없어요).

#include <iostream>
#include <string>

int main()
{
    std::string s1 = "Test";
//  std::string&& r1 = s1;           // error: can't bind to lvalue

    const std::string& r2 = s1 + s1; // okay: lvalue reference to const extends lifetime
//  r2 += "Test";                    // error: can't modify through reference to const

    std::string&& r3 = s1 + s1;      // okay: rvalue reference extends lifetime
    r3 += "Test";                    // okay: can modify through reference to non-const
    std::cout << r3 << '\n';
}

더 중요하게, 함수에 rvalue 참조 오버로드와 lvalue 참조 오버로드가 모두 있으면, rvalue 참조 오버로드는 rvalue(prvalue와 xvalue 모두)에 결합하고 lvalue 참조 오버로드는 lvalue에 결합해요.

#include <iostream>
#include <utility>

void f(int& x)
{
    std::cout << "lvalue reference overload f(" << x << ")\n";
}

void f(const int& x)
{
    std::cout << "lvalue reference to const overload f(" << x << ")\n";
}

void f(int&& x)
{
    std::cout << "rvalue reference overload f(" << x << ")\n";
}

int main()
{
    int i = 1;
    const int ci = 2;

    f(i);  // calls f(int&)
    f(ci); // calls f(const int&)
    f(3);  // calls f(int&&)
           // would call f(const int&) if f(int&&) overload wasn't provided
    f(std::move(i)); // calls f(int&&)

    // rvalue reference variables are lvalues when used in expressions
    int&& x = 1;
    f(x);            // calls f(int& x)
    f(std::move(x)); // calls f(int&& x)
}

이를 통해 이동 생성자, 이동 할당 연산자, 그리고 다른 이동 인지 함수(예: std::vector::push_back())가 적절할 때 자동으로 선택되게 할 수 있어요.

rvalue 참조는 xvalue에 결합할 수 있으므로 비임시 객체를 가리킬 수 있어요.

int i2 = 42;
int&& rri = std::move(i2); // binds directly to i2

이로써 더 이상 필요 없는 스코프 안 객체에서 이동할 수 있게 돼요.

std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> v2(std::move(v)); // binds an rvalue reference to v
assert(v.empty());

전달 참조 (Forwarding references)

전달 참조(forwarding reference)는 함수 인자의 값 카테고리를 보존하는 특별한 종류의 참조로, std::forward를 통해 전달할 수 있게 해줘요. 전달 참조는 다음 중 하나예요.

  1. 같은 함수 템플릿의 cv-unqualified 타입 템플릿 매개변수에 대한 rvalue 참조로 선언된 함수 템플릿의 함수 매개변수:
template<class T>
int f(T&& x)                      // x is a forwarding reference
{
    return g(std::forward<T>(x)); // and so can be forwarded
}

int main()
{
    int i;
    f(i); // argument is lvalue, calls f<int&>(int&), std::forward<int&>(x) is lvalue
    f(0); // argument is rvalue, calls f<int>(int&&), std::forward<int>(x) is rvalue
}

template<class T>
int g(const T&& x); // x is not a forwarding reference: const T is not cv-unqualified

template<class T>
struct A
{
    template<class U>
    A(T&& x, U&& y, int* p); // x is not a forwarding reference: T is not a
                             // type template parameter of the constructor,
                             // but y is a forwarding reference
};
  1. auto&& — 단, 중괄호로 감싼 초기화자 목록에서 추론되거나, 클래스 템플릿 인자 추론 중 클래스 템플릿의 템플릿 매개변수를 나타낼 때는 제외(since C++17):
auto&& vec = foo();       // foo() may be lvalue or rvalue, vec is a forwarding reference
auto i = std::begin(vec); // works either way
(*i)++;                   // works either way

g(std::forward<decltype(vec)>(vec)); // forwards, preserving value category

for (auto&& x: f())
{
    // x is a forwarding reference; this is a common way to use range for in generic code
}

auto&& z = {1, 2, 3}; // *not* a forwarding reference (special case for initializer lists)

템플릿 인자 추론과 std::forward도 참고하세요. (since C++11)

댕글링 참조 (Dangling references)

참조는 초기화 시 항상 유효한 객체나 함수를 가리키지만, 가리키는 객체의 수명이 끝났는데도 참조가 접근 가능한 상태(댕글링)의 프로그램을 만들 수 있어요.

참조 타입의 표현식 expr이 주어지고, 참조가 가리키는 객체나 함수를 target이라고 하면:

  • expr의 평가 문맥에서 target에 대한 포인터가 유효하면, 결과는 target을 가리켜요.
  • 그 외, 동작은 undefined예요.
std::string& f()
{
    std::string s = "Example";
    return s; // exits the scope of s:
              // its destructor is called and its storage deallocated
}

std::string& r = f(); // dangling reference
std::cout << r;       // undefined behavior: reads from a dangling reference
std::string s = f();  // undefined behavior: copy-initializes from a dangling reference

rvalue 참조와 const에 대한 lvalue 참조는 임시 객체의 수명을 확장한다는 점을 기억하세요(규칙과 예외는 참조 초기화 참고).

가리키는 객체가 파괴되었지만(예: 명시적 소멸자 호출) 저장 공간이 해제되지 않았다면, 수명이 끝난 객체에 대한 참조를 제한된 방식으로 쓸 수 있고, 같은 저장 공간에 객체가 재생성되면 유효해질 수 있어요(수명 밖 접근 참고).

타입 비접근 참조 (Type-inaccessible references)

변환된 초기화자가 lvalue(until C++11)glvalue(since C++11)인 객체에 참조를 결합하려 하는데 그를 통해 객체가 타입 비접근(type-inaccessible)이면 undefined behavior예요.

char x alignas(int);

int& ir = *reinterpret_cast<int*>(&x); // undefined behavior:
                                       // initializer refers to char object

호출 비호환 참조 (Call-incompatible references)

변환된 초기화자가 lvalue(until C++11)glvalue(since C++11)이고, 그 타입이 함수 정의의 타입과 호출 비호환(call-incompatible)인 함수에 참조를 결합하려 하면 undefined behavior예요.

void f(int);

using F = void(float);
F& ir = *reinterpret_cast<F*>(&f); // undefined behavior:
                                   // initializer refers to void(int) function

참고 (Notes)

  • 기능 테스트 매크로:
    • __cpp_rvalue_references 200610L (C++11) Rvalue references

결함 보고서 (Defect reports)

  • CWG 453 (C++98): 참조가 어떤 객체나 함수에 결합될 수 없는지가 불명확했음 → 명확히 함.
  • CWG 1510 (C++11): decltype의 피연산자에서 cv-qualified 참조를 만들 수 없었음 → 허용.
  • CWG 2550 (C++98): 매개변수가 "void에 대한 참조" 타입일 수 있었음 → 금지.
  • CWG 2933 (C++98): 댕글링 참조 접근 동작이 불명확했음 → 명확히 함.

더 알아보기