reference_initialization — 참조 초기화
reference_initialization — 참조 초기화 (Reference initialization)
**참조 초기화(reference initialization)**는 참조(reference)를 객체에 바인딩하는 초기화예요. T& ref = target; 또는 T&& ref = target;처럼 참조를 선언할 때 적용돼요.
참조는 일단 초기화되면 다른 객체를 가리키도록 바꿀 수 없어요. lvalue 참조는 lvalue에, rvalue 참조는 임시 객체(rvalue)에 바인딩되는 규칙을 이해하는 것이 핵심이에요.
출처: cppreference
본문
참조 초기화는 참조를 객체에 바인딩해요.
문법 (Syntax)
lvalue/rvalue 참조와 목록·지정 목록 초기화의 여러 형태가 있어요. 기본 형태는 다음과 같아요.
T& ref = target;
T& ref( target );
T&& ref = target;
T&& ref( target );
func-refparam( target ); // 참조형 매개변수에 인자 전달
return target; // 참조형 반환
Class::Class(...) : ref-member( target ) { ... } // 참조형 멤버 초기화
T에 대한 참조는 T 타입 객체, T 타입 함수, 또는 T로 암시적 변환 가능한 객체로 초기화될 수 있어요. 일단 초기화되면 참조는 다른 객체를 가리키도록 바꿀 수 없어요.
정의 (Definitions)
두 타입 T1, T2에 대해:
T1과T2의 cv 한정 없는 버전을 각각U1,U2라 할 때,U1이U2와 유사(similar)하거나U1이U2의 기반 클래스면,T1은T2에 reference-related예요.- "
T2에 대한 포인터" 타입의 prvalue가 표준 변환 시퀀스로 "T1에 대한 포인터" 타입으로 변환될 수 있으면,T1은T2에 reference-compatible이에요.
초기화 규칙 (Initialization rules)
참조 초기화가 목록 또는 지정 목록 초기화를 사용하면 목록 초기화의 규칙을 따라요.
비목록 참조 초기화에서, target의 타입을 U라 할 때 참조는 target에 **직접 바인딩(direct binding)**되거나, target에서 변환된 T 타입 값에 바인딩돼요. 직접 바인딩이 먼저 고려되고, 그 다음 간접 바인딩이 고려되며, 둘 다 없으면 프로그램은 잘못된 형식이에요.
직접 바인딩
다음 조건이 모두 만족하면 참조는 target(또는 그 적절한 기반 클래스 부분 객체)에 직접 바인딩돼요.
- 초기화되는 참조가 lvalue 참조
target이 비트 필드가 아닌 lvalueT가U와 reference-compatible
또는 target이 클래스 타입이고, T가 U에 reference-related가 아니며, target이 T와 reference-compatible인 타입 V의 lvalue로 변환될 수 있다면, 그 변환의 lvalue 결과(또는 그 기반 클래스 부분 객체)에 참조를 바인딩해요.
또한 모든 조건이 만족하면 rvalue 참조도 target(또는 그 기반 클래스 부분 객체)에 직접 바인딩돼요.
target이 비트 필드가 아닌 xvalue / rvalue / 클래스 prvalue / 배열 prvalue / 함수 lvalue 등T가U와 reference-compatible
C++17부터 target이 prvalue면 임시 구체화가 적용되고, 참조는 결과 객체(또는 그 기반 클래스 부분 객체)에 바인딩돼요.
간접 바인딩
직접 바인딩이 없으면 간접 바인딩을 고려해요. 이 경우 T는 U에 reference-related일 수 없어요. T 또는 U가 클래스 타입이면, 사용자 정의 변환이 고려돼요. 그 변환 결과가 T 타입의 prvalue로 암시적으로 변환되고 임시 구체화가 적용되어, 참조는 결과 객체에 바인딩돼요.
임시 수명
참조에 바인딩된 임시 객체는 기본적으로 참조가 유효한 동안 살아 있어요. 다만 다음 예외가 있어요.
- return 문에서 함수의 반환 값에 바인딩된 임시는 연장되지 않아요. 즉 그 return 문은 항상 매달린 참조를 반환해요.
- 함수 호출에서 참조 매개변수에 바인딩된 임시는 그 함수 호출을 포함하는 전체 표현식이 끝날 때까지 존재해요.
- new 표현식의 초기화자에서 참조에 바인딩된 임시는 그 new 표현식을 포함하는 전체 표현식 끝까지 존재해요.
- C++20부터, 직접 초기화 문법(괄호)으로 초기화된 애그리거트의 참조 요소에 바인딩된 임시는 초기화자를 포함하는 전체 표현식 끝까지 존재해요 (목록 초기화 중괄호와는 달라요).
예제 (Example)
#include <sstream>
#include <utility>
struct S
{
int mi;
const std::pair<int, int>& mp; // 참조 멤버
};
void foo(int) {}
struct A {};
struct B : A
{
int n;
operator int&() { return n; }
};
B bar() { return B(); }
//int& bad_r; // 오류: 초기화자 없음
extern int& ext_r; // OK
int main()
{
// Lvalues
int n = 1;
int& r1 = n; // 객체 n 에 대한 lvalue 참조
const int& cr(n); // 참조는 더 cv 한정될 수 있음
volatile int& cv{n}; // 어떤 초기화 문법이든 사용 가능
int& r2 = r1; // 객체 n 에 대한 또 다른 lvalue 참조
// int& bad = cr; // 오류: 덜 cv 한정됨
int& r3 = const_cast<int&>(cr); // const_cast 필요
void (&rf)(int) = foo; // 함수에 대한 lvalue 참조
int ar[3];
int (&ra)[3] = ar; // 배열에 대한 lvalue 참조
B b;
A& base_ref = b; // 기반 부분 객체에 대한 참조
int& converted_ref = b; // 변환 결과에 대한 참조
// Rvalues
// int& bad = 1; // 오류: rvalue 에 lvalue 참조 바인딩 불가
const int& cref = 1; // rvalue 에 바인딩
int&& rref = 1; // rvalue 에 바인딩
const A& cref2 = bar(); // B 임시의 A 부분 객체에 대한 참조
A&& rref2 = bar(); // 동일
int&& xref = static_cast<int&&>(n); // n 에 직접 바인딩
// int&& copy_ref = n; // 오류: lvalue 에 바인딩 불가
double&& copy_ref = n; // 값 1.0 의 rvalue 임시에 바인딩
// 임시 수명 제한
// std::ostream& buf_ref = std::ostringstream() << 'a';
// ostringstream 임시는 operator<< 의 왼쪽 피연산자에
// 바인딩됐지만 그 수명이 세미콜론에서 끝나
// buf_ref 는 매달린 참조
S a {1, {2, 3}}; // 임시 pair {2, 3} 이 참조 멤버 a.mp 에 바인딩되고
// 그 수명이 객체 a 의 수명과 일치하도록 연장됨
S* p = new S{1, {2, 3}}; // 임시 pair {2, 3} 이 참조 멤버 p->mp 에
// 바인딩되지만 그 수명이 세미콜론에서 끝나
// p->mp 는 매달린 참조
delete p;
// [[maybe_unused]] 흉내내기
[](...){}
(
cv, r2, r3, rf, ra, base_ref, converted_ref,
a, cref, rref, cref2, rref2, copy_ref, xref
);
}
핵심 포인트예요. int& r1 = n처럼 lvalue 참조는 lvalue에 바인딩되지만, int& bad = 1처럼 임시(rvalue)에는 바인딩될 수 없어요(비const lvalue 참조). 반면 const int& cref = 1은 rvalue에 바인딩되고, int&& rref = 1은 rvalue 참조로 바인딩돼요. 또 B b; int& converted_ref = b;처럼 변환 함수를 통해 int&에 바인딩되는 것도 볼 수 있어요. new로 만든 객체의 참조 멤버는 임시 수명이 일찍 끝나 매달린 참조가 될 수 있음을 주의해야 해요.