참조 초기화
참조 초기화 (Reference initialization)
참조를 선언할 때 결국 "어떤 객체에 묶일지"가 정해져요. 이 페이지에서는 참조가 어떻게 초기화되는지, 즉 lvalue 참조·rvalue 참조가 어떤 대상에 직접 결합(direct binding)되거나 간접 결합(indirect binding)되는지, 그리고 임시 객체의 수명이 어떻게 확장되는지를 설명할게요.
출처: cppreference
본문
참조는 T 타입의 객체, T 타입의 함수, 또는 T로 암시 변환 가능한 객체로 초기화할 수 있어요. 일단 초기화되면 참조는 다른 객체를 가리키도록 다시 자리(reseat)를 바꿀 수 없어요.
참조는 다음 상황에서 초기화돼요.
- 이름 있는 lvalue 참조 변수를 초기화자와 함께 선언할 때.
- 이름 있는 rvalue 참조 변수를 초기화자와 함께 선언할 때.
- 함수 호출 표현식에서 함수 매개변수가 참조 타입일 때.
- return 문에서 함수가 참조 타입을 반환할 때. 반환된 참조가 임시 표현식의 결과에 결합되면 프로그램은 ill-formed예요. (since C++26)
- 참조 타입의 비정적 데이터 멤버를 멤버 초기화자로 초기화할 때.
구문 (Syntax)
비목록 초기화(Non-list-initialization):
T& ref = target ;
T& ref ( target ); (1)
T&& ref = target ;
T&& ref ( target ); (2) (since C++11)
func-refpar ( target ) (3)
return target ; (4) (inside the definition of func-refret)
Class::Class(...) : ref-member ( target ) { ... } (5) (inside the definition of Class)
일반 목록 초기화(Ordinary list-initialization) (since C++11):
T& ref = { arg1, arg2, ... };
T& ref { arg1, arg2, ... }; (1)
T&& ref = { arg1, arg2, ... };
T&& ref { arg1, arg2, ... }; (2)
func-refpar ({ arg1, arg2, ... }); (3)
지정 초기화(Designated list-initialization) (since C++20):
T& ref = {.des1 = arg1 , .des2 { arg2 } ... };
T& ref {.des1 = arg1 , .des2 { arg2 } ... }; (1)
T&& ref = {.des1 = arg1 , .des2 { arg2 } ... };
T&& ref {.des1 = arg1 , .des2 { arg2 } ... }; (2)
func-refpar ({.des1 = arg1 , .des2 { arg2 } ... }); (3)
여기서 T는 참조 대상 타입, ref는 초기화할 참조 변수, target은 쓰이는 초기화자 표현식, func-refpar는 참조 타입(T& 또는 T&&(since C++11))의 매개변수를 가진 함수, func-refret는 반환 타입이 참조 타입인 함수, Class는 클래스 이름, ref-member는 Class의 참조 타입 비정적 데이터 멤버, des1, des2, ...는 지정자, arg1, arg2, ...는 초기화자 목록의 초기화자예요.
정의 (Definitions)
두 타입 T1, T2에 대해:
T1,T2의 cv-unqualified 버전을 각각U1,U2라 할 때,U1이U2와 유사(similar)하거나U1이U2의 베이스 클래스이면T1은T2에 reference-related예요.- "
T2에 대한 포인터" 타입의 prvalue가 표준 변환 시퀀스를 통해 "T1에 대한 포인터" 타입으로 변환될 수 있으면,T1은T2와 reference-compatible예요.
초기화 규칙 (Initialization rules)
참조 초기화가 일반 또는 지정(since C++20) 목록 초기화를 쓰면, 목록 초기화의 규칙을 따라요. (since C++11)
비목록 참조 초기화에서는 target의 타입을 U라 할 때, 참조는 target에 직접 결합하거나 target에서 변환된 T 타입 값에 결합해요. 직접 결합을 먼저 고려하고, 직접 결합이 불가능하면 간접 결합을 고려해요. 둘 다 가능하지 않으면 프로그램은 ill-formed예요.
두 타입의 reference-compatible 관계로 참조 결합의 유효성을 확립하는데 표준 변환 시퀀스가 ill-formed면, 그런 결합이 필요한 프로그램은 ill-formed예요.
직접 결합 (Direct binding)
다음 조건을 모두 만족하면:
- 초기화할 참조가 lvalue 참조,
target이 비트 필드가 아닌 lvalue,T가U와 reference-compatible,
참조는 target 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요.
double d = 2.0;
double& rd = d; // rd refers to d
const double& rcd = d; // rcd refers to d
struct A {};
struct B : A {} b;
A& ra = b; // ra refers to A subobject in b
const A& rca = b; // rca refers to A subobject in b
그 외, 다음 조건을 모두 만족하면:
- 초기화할 참조가 lvalue 참조,
U가 클래스 타입,T가U에 reference-related가 아님,target이T가V와 reference-compatible인V타입 lvalue로 변환될 수 있음,
참조는 변환 결과의 lvalue 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요.
struct A {};
struct B : A { operator int&(); };
int& ir = B(); // ir refers to the result of B::operator int&
그 외, 초기화할 참조가 lvalue 참조이고 T가 const-qualified가 아니거나 volatile-qualified면, 프로그램은 ill-formed예요.
double& rd2 = 2.0; // error: not an lvalue and reference is not const
int i = 2;
double& rd3 = i; // error: type mismatch and reference is not const
그 외, 다음 조건을 모두 만족하면:
target이 다음 카테고리 중 하나의 값:- rvalue (until C++11)
- 비트 필드가 아닌 xvalue
- 클래스 prvalue
- 배열 prvalue
- 함수 lvalue (since C++11)(until C++17)
- 비트 필드가 아닌 rvalue
- 함수 lvalue (since C++17)
T가U와 reference-compatible,
참조는 target 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요.
struct A {};
struct B : A {};
extern B f();
const A& rca2 = f(); // bound to the A subobject of the B rvalue.
A&& rra = f(); // same as above
int i2 = 42;
int&& rri = static_cast<int&&>(i2); // bound directly to i2
target이 prvalue면 임시 구체화(temporary materialization)가 적용되는데, prvalue의 타입을 조정 타입 P로 간주해요.
P는target의 타입(즉U)에T의 cv-qualification을 더해 조정돼요.
이 경우 참조는 결과 객체 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요. (since C++17)
그 외, 다음 조건을 모두 만족하면:
U가 클래스 타입,T가U에 reference-related가 아님,target이T가V와 reference-compatible인V타입 값v로 변환될 수 있고,v가 다음 카테고리 중 하나:- rvalue (until C++11)
- xvalue
- 클래스 prvalue
- 함수 lvalue (since C++11)(until C++17)
- rvalue
- 함수 lvalue (since C++17)
참조는 변환 결과 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요.
struct A {};
struct B : A {};
struct X { operator B(); } x;
const A& r = x; // bound to the A subobject of the result of the conversion
B&& rrb = x; // bound directly to the result of the conversion
변환 결과가 prvalue면 임시 구체화가 적용되는데, prvalue의 타입을 조정 타입 P로 간주해요.
P는 변환 결과의 타입에T의 cv-qualification을 더해 조정돼요.
이 경우 참조는 결과 객체 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요. (since C++17)
간접 결합 (Indirect binding)
직접 결합이 가능하지 않으면 간접 결합을 고려해요. 이 경우 T는 U에 reference-related일 수 없어요.
T 또는 U가 클래스 타입이면, 사용자 정의 변환은 T 타입 객체의 복사 초기화 규칙을 사용해 고려돼요. 대응하는 비참조 복사 초기화가 ill-formed면 프로그램은 ill-formed예요. 변환 함수 호출의 결과(비참조 복사 초기화에 대해 설명한 대로)는 그다음 참조를 직접 초기화하는 데 사용돼요. 이 직접 초기화에서는 사용자 정의 변환을 고려하지 않아요.
- 그 외,
T타입 임시 객체가 만들어지고target에서 복사 초기화돼요. 참조는 그 임시 객체에 결합돼요. (until C++17) - 그 외,
target이 "cv-unqualifiedT" 타입의 prvalue로 암시 변환돼요. 임시 구체화 변환이 적용되고, prvalue의 타입을T로 간주하며, 참조는 결과 객체에 결합돼요. (since C++17)
const std::string& rs = "abc"; // rs refers to temporary copy-initialized from char array
const double& rcd2 = 2; // rcd2 refers to temporary with value 2.0
int i3 = 2;
double&& rrd3 = i3; // rrd3 refers to temporary with value 2.0
임시 객체의 수명 (Lifetime of a temporary)
참조가 임시 객체 또는 그것의 하위 객체에 결합될 때마다, 임시 객체의 수명은 참조의 수명과 일치하도록 확장돼요(임시 객체 수명 예외 참고). 여기서 임시 객체 또는 그것의 하위 객체는 다음 표현식 중 하나로 표시돼요.
- 객체 타입의 prvalue 표현식, (until C++17)
- 임시 구체화 변환, (since C++17)
- 이 표현식들 중 하나인
e에 대한 괄호 표현식(e), a가 배열이고 이 표현식들 중 하나인 내장 첨자 표현식a[n]또는n[a],e가 이 표현식들 중 하나이고m이 객체 타입의 비정적 데이터 멤버를 지정하는 클래스 멤버 접근 표현식e.m,e가 이 표현식들 중 하나이고mp가 데이터 멤버에 대한 포인터인 멤버 포인터 연산e.*mp,- 사용자 정의 변환 없이 이 표현식들 중 하나를, 피연산자가 지정하는 객체 또는 그것의 완전 객체나 하위 객체를 가리키는 glvalue로 변환하는
const_cast,static_cast,dynamic_cast,reinterpret_cast변환(명시적 캐스트 표현식은 이 캐스트들의 시퀀스로 해석됨), - glvalue인 조건 표현식
cond ? e1 : e2(여기서e1또는e2가 이 표현식들 중 하나), - glvalue인 내장 쉼표 표현식
x, e(여기서e가 이 표현식들 중 하나).
이 수명 규칙에는 다음 예외가 있어요.
- return 문에서 함수의 반환 값에 결합된 임시 객체는 확장되지 않아요. return 표현식 끝에서 즉시 파괴돼요. 그런 return 문은 항상 댕글링 참조를 반환해요. (until C++26)
- 함수 호출에서 참조 매개변수에 결합된 임시 객체는 그 함수 호출을 포함하는 전체 표현식이 끝날 때까지 존재해요. 함수가 참조를 반환해서 그것이 전체 표현식보다 오래 살면, 댕글링 참조가 돼요.
- new 표현식의 초기화자에서 참조에 결합된 임시 객체는 그 new 표현식을 포함하는 전체 표현식이 끝날 때까지 존재하고, 초기화된 객체만큼 오래 있지 않아요. 초기화된 객체가 전체 표현식보다 오래 살면, 그 참조 멤버는 댕글링 참조가 돼요. (since C++11)
- 직접 초기화 문법(괄호)으로 초기화된 집합체(aggregate)의 참조 요소에 결합된 임시 객체는, 목록 초기화 문법(중괄호)과 달리, 초기화자를 포함하는 전체 표현식이 끝날 때까지 존재해요.
struct A
{
int&& r;
};
A a1{7}; // OK, lifetime is extended
A a2(7); // well-formed, but dangling reference
일반적으로 임시 객체의 수명은 "그것을 넘겨주기"로 더 확장할 수 없어요. 임시 객체가 결합된 참조 변수나 데이터 멤버에서 초기화된 두 번째 참조는 그 수명에 영향을 주지 않아요.
참고 (Notes)
참조는 초기화자 없이 함수 매개변수 선언, 함수 반환 타입 선언, 클래스 멤버 선언, 그리고 extern 지정자와 함께만 나타날 수 있어요.
CWG 1696 이슈가 해결되기 전까지는 임시 객체가 생성자 초기화자 목록의 참조 멤버에 결합되는 것이 허용됐고, 그것은 객체가 존재하는 동안이 아니라 생성자가 끝날 때까지만 지속됐어요. CWG 1696 이후 그런 초기화는 ill-formed인데, 많은 컴파일러가 여전히 지원해요(눈에 띄는 예외는 clang).
예제 (Example)
#include <sstream>
#include <utility>
struct S
{
int mi;
const std::pair<int, int>& mp; // reference member
};
void foo(int) {}
struct A {};
struct B : A
{
int n;
operator int&() { return n; }
};
B bar() { return B(); }
//int& bad_r; // error: no initializer
extern int& ext_r; // OK
int main()
{
// Lvalues
int n = 1;
int& r1 = n; // lvalue reference to the object n
const int& cr(n); // reference can be more cv-qualified
volatile int& cv{n}; // any initializer syntax can be used
int& r2 = r1; // another lvalue reference to the object n
// int& bad = cr; // error: less cv-qualified
int& r3 = const_cast<int&>(cr); // const_cast is needed
void (&rf)(int) = foo; // lvalue reference to function
int ar[3];
int (&ra)[3] = ar; // lvalue reference to array
B b;
A& base_ref = b; // reference to base subobject
int& converted_ref = b; // reference to the result of a conversion
// Rvalues
// int& bad = 1; // error: cannot bind lvalue ref to rvalue
const int& cref = 1; // bound to rvalue
int&& rref = 1; // bound to rvalue
const A& cref2 = bar(); // reference to A subobject of B temporary
A&& rref2 = bar(); // same
int&& xref = static_cast<int&&>(n); // bind directly to n
// int&& copy_ref = n; // error: can't bind to an lvalue
double&& copy_ref = n; // bind to an rvalue temporary with value 1.0
// Restrictions on temporary lifetimes
// std::ostream& buf_ref = std::ostringstream() << 'a';
// the ostringstream temporary was bound to the left operand
// of operator<< but its lifetime ended at the semicolon so
// the buf_ref is a dangling reference
S a {1, {2, 3}}; // temporary pair {2, 3} bound to the reference member
// a.mp and its lifetime is extended to match
// the lifetime of object a
S* p = new S{1, {2, 3}}; // temporary pair {2, 3} bound to the reference
// member p->mp, but its lifetime ended at the semicolon
// p->mp is a dangling reference
delete p;
// Imitate [[maybe_unused]] applied to the following variables:
[](...){}
(
cv, r2, r3, rf, ra, base_ref, converted_ref,
a, cref, rref, cref2, rref2, copy_ref, xref
);
}
결함 보고서 (Defect reports)
- CWG 391 (C++98): const-qualified 타입에 대한 참조를 클래스 타입 rvalue로 초기화하면 임시 객체가 만들어질 수 있고, rvalue를 그 임시 객체에 복사하려면 그 클래스의 생성자가 필요했음 → 임시 객체를 만들지 않고 생성자도 필요 없음.
- CWG 450 (C++98): const-qualified 배열에 대한 참조를 reference-compatible 배열 rvalue로 초기화할 수 없었음 → 허용.
- CWG 589 (C++98): 참조가 배열이나 클래스 rvalue에 직접 결합할 수 없었음 → 허용.
- CWG 656 (C++98): reference-compatible가 아니지만 reference-compatible 타입으로의 변환 함수가 있는 타입으로 초기화된 const-qualified 참조가, 변환 함수의 반환 값(또는 그 베이스 클래스 하위 객체)에서 복사된 임시 객체에 결합했음 → 반환 값(또는 그 베이스 클래스 하위 객체)에 직접 결합.
- CWG 1287 (C++11): 클래스 타입의 target에서 다른 reference-compatible 타입으로의 변환이 암시적일 수만 있었음 → 명시적 변환도 허용.
- CWG 1295 (C++11): 참조가 비트 필드 xvalue에 결합할 수 있었음 → 금지.
- CWG 1299 (C++98): 임시 객체의 정의가 불명확했음 → 명확히 함.
- CWG 1571 (C++98): 간접 결합의 사용자 정의 변환이 target의 타입을 고려하지 않았음 → 고려.
- CWG 1604 (C++98): 사용자 정의 변환이 간접 결합에서 고려되지 않았음 → 고려.
- CWG 2352 (C++98): reference compatibility가 qualification 변환을 고려하지 않았음 → 고려.
- CWG 2481 (C++17): 간접 결합에서 임시 구체화 결과 타입에 cv-qualification이 추가되지 않았음 → 추가.
- CWG 2657 (C++17): 직접 결합에서 임시 구체화 결과 타입에 cv-qualification이 추가되지 않았음 → 추가.
- CWG 2801 (C++98): reference-related 타입이 간접 결합에 허용됐음 → 금지.