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에 대해:

  • T1T2의 cv 한정 없는 버전을 각각 U1, U2라 할 때, U1U2와 유사(similar)하거나 U1U2의 기반 클래스면, T1T2reference-related예요.
  • "T2에 대한 포인터" 타입의 prvalue가 표준 변환 시퀀스로 "T1에 대한 포인터" 타입으로 변환될 수 있으면, T1T2reference-compatible이에요.

초기화 규칙 (Initialization rules)

참조 초기화가 목록 또는 지정 목록 초기화를 사용하면 목록 초기화의 규칙을 따라요.

비목록 참조 초기화에서, target의 타입을 U라 할 때 참조는 target에 **직접 바인딩(direct binding)**되거나, target에서 변환된 T 타입 값에 바인딩돼요. 직접 바인딩이 먼저 고려되고, 그 다음 간접 바인딩이 고려되며, 둘 다 없으면 프로그램은 잘못된 형식이에요.

직접 바인딩

다음 조건이 모두 만족하면 참조는 target(또는 그 적절한 기반 클래스 부분 객체)에 직접 바인딩돼요.

  • 초기화되는 참조가 lvalue 참조
  • target이 비트 필드가 아닌 lvalue
  • TU와 reference-compatible

또는 target이 클래스 타입이고, TU에 reference-related가 아니며, targetT와 reference-compatible인 타입 V의 lvalue로 변환될 수 있다면, 그 변환의 lvalue 결과(또는 그 기반 클래스 부분 객체)에 참조를 바인딩해요.

또한 모든 조건이 만족하면 rvalue 참조도 target(또는 그 기반 클래스 부분 객체)에 직접 바인딩돼요.

  • target이 비트 필드가 아닌 xvalue / rvalue / 클래스 prvalue / 배열 prvalue / 함수 lvalue 등
  • TU와 reference-compatible

C++17부터 target이 prvalue면 임시 구체화가 적용되고, 참조는 결과 객체(또는 그 기반 클래스 부분 객체)에 바인딩돼요.

간접 바인딩

직접 바인딩이 없으면 간접 바인딩을 고려해요. 이 경우 TU에 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로 만든 객체의 참조 멤버는 임시 수명이 일찍 끝나 매달린 참조가 될 수 있음을 주의해야 해요.

더 알아보기 (Learn more)

cppreference