참조
참조 (Reference)
참조는 이미 존재하는 객체나 함수의 별칭(alias) 역할을 하는 이름 있는 변수예요. 값으로 복사하는 대신 원본을 그대로 다루고 싶을 때, 특히 함수 인자나 반환 값으로 쓸 때 유용해요. 이 페이지에서는 참조 선언 문법, 참조 축약(reference collapsing), lvalue·rvalue·전달 참조까지 설명할게요.
출처: cppreference
본문
참조 선언은 declarator가 다음 형태를 가진 어떤 simple-declaration이에요.
& attr (optional) declarator (1)
&& attr (optional) declarator (2) (since C++11)
- lvalue 참조 declarator: 선언
S& D;는D를 decl-specifier-seqS가 정하는 타입에 대한 lvalue 참조로 선언해요. - rvalue 참조 declarator: 선언
S&& D;는D를S가 정하는 타입에 대한 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를 통해 전달할 수 있게 해줘요. 전달 참조는 다음 중 하나예요.
- 같은 함수 템플릿의 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
};
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_references200610L (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): 댕글링 참조 접근 동작이 불명확했음 → 명확히 함.