참조 초기화

참조 초기화 (Reference initialization)

참조를 선언할 때 결국 "어떤 객체에 묶일지"가 정해져요. 이 페이지에서는 참조가 어떻게 초기화되는지, 즉 lvalue 참조·rvalue 참조가 어떤 대상에 직접 결합(direct binding)되거나 간접 결합(indirect binding)되는지, 그리고 임시 객체의 수명이 어떻게 확장되는지를 설명할게요.

출처: cppreference

본문

참조는 T 타입의 객체, T 타입의 함수, 또는 T로 암시 변환 가능한 객체로 초기화할 수 있어요. 일단 초기화되면 참조는 다른 객체를 가리키도록 다시 자리(reseat)를 바꿀 수 없어요.

참조는 다음 상황에서 초기화돼요.

  1. 이름 있는 lvalue 참조 변수를 초기화자와 함께 선언할 때.
  2. 이름 있는 rvalue 참조 변수를 초기화자와 함께 선언할 때.
  3. 함수 호출 표현식에서 함수 매개변수가 참조 타입일 때.
  4. return 문에서 함수가 참조 타입을 반환할 때. 반환된 참조가 임시 표현식의 결과에 결합되면 프로그램은 ill-formed예요. (since C++26)
  5. 참조 타입의 비정적 데이터 멤버를 멤버 초기화자로 초기화할 때.

구문 (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-memberClass의 참조 타입 비정적 데이터 멤버, des1, des2, ...는 지정자, arg1, arg2, ...는 초기화자 목록의 초기화자예요.

정의 (Definitions)

두 타입 T1, T2에 대해:

  • T1, T2의 cv-unqualified 버전을 각각 U1, U2라 할 때, U1U2와 유사(similar)하거나 U1U2의 베이스 클래스이면 T1T2에 reference-related예요.
  • "T2에 대한 포인터" 타입의 prvalue가 표준 변환 시퀀스를 통해 "T1에 대한 포인터" 타입으로 변환될 수 있으면, T1T2와 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,
  • TU와 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가 클래스 타입,
  • TU에 reference-related가 아님,
  • targetTV와 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)
  • TU와 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로 간주해요.

  • Ptarget의 타입(즉 U)에 T의 cv-qualification을 더해 조정돼요.

이 경우 참조는 결과 객체 또는 그것의 적절한 베이스 클래스 하위 객체에 결합해요. (since C++17)

그 외, 다음 조건을 모두 만족하면:

  • U가 클래스 타입,
  • TU에 reference-related가 아님,
  • targetTV와 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)

직접 결합이 가능하지 않으면 간접 결합을 고려해요. 이 경우 TU에 reference-related일 수 없어요.

T 또는 U가 클래스 타입이면, 사용자 정의 변환은 T 타입 객체의 복사 초기화 규칙을 사용해 고려돼요. 대응하는 비참조 복사 초기화가 ill-formed면 프로그램은 ill-formed예요. 변환 함수 호출의 결과(비참조 복사 초기화에 대해 설명한 대로)는 그다음 참조를 직접 초기화하는 데 사용돼요. 이 직접 초기화에서는 사용자 정의 변환을 고려하지 않아요.

  • 그 외, T 타입 임시 객체가 만들어지고 target에서 복사 초기화돼요. 참조는 그 임시 객체에 결합돼요. (until C++17)
  • 그 외, target이 "cv-unqualified T" 타입의 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 타입이 간접 결합에 허용됐음 → 금지.

더 알아보기