집합체 초기화

집합체 초기화 (Aggregate initialization)

int a[] = {1, 2, 3}; 같은 중괄호 초기화는 C에서부터 익숙한데요, C++에서는 이게 '집합체(aggregate)'라는 특별한 타입에 대해 정의된 초기화 규칙예요. 이 페이지에서 집합체가 도대체 뭔지, 중괄호 안의 값들이 어떤 순서와 규칙으로 각 요소를 채우는지 정리해 볼게요.

출처: cppreference

본문

집합체 초기화는 초기화 리스트로 집합체를 초기화하는 거예요. 리스트 초기화(list-initialization)의 한 형태예요.(C++11부터)

문법

T object = { arg1, arg2, ... }; (1)
T object { arg1, arg2, ... }; (2) (C++11부터)
T object = { .designator1 = arg1 , .designator2 { arg2 } ... }; (3) (C++20부터)
T object { .designator1 = arg1 , .designator2 { arg2 } ... }; (4) (C++20부터)
T object = { arg1, arg2, ... , .designator1 = arg1 , .designator2 { arg2 } ... }; (5) (C++29부터)
T object { arg1, arg2, ... , .designator1 = arg1 , .designator2 { arg2 } ... }; (6) (C++29부터)

1,2) 일반 초기화 리스트로 집합체를 초기화해요. 지시자(designator)는 허용되지 않아요. 3,4) 지시된 초기화(designated initializer)로 집합체를 초기화해요(집합체 클래스만). 5,6) 일반 초기화 리스트(기반 클래스만 초기화해야 함)로 먼저 초기화하고, 이어서 지시된 초기화로 초기화해요.

설명

집합체 초기화는 집합체의 각 요소를 초기화 리스트의 대응하는 초기화 절(clause)로 요소 순서대로 초기화해요. 집합체(aggregate)는 배열이거나 특정 기준을 만족하는 클래스예요(아래 참고).

집합체 초기화의 효과는 다음과 같아요.

  • 각 초기화 절(지시자가 없는 것)(C++20부터)은 배열 첨자/클래스 정의 등장 순서대로 집합체 요소를 복사 초기화(copy-initialize)해요.
  • 초기화 절이 표현식이라면:
    • 복사 초기화 규칙에 따라 암시적 변환이 허용돼요. 단, 축소 변환(narrowing conversion)은 금지돼요.(C++11부터)
    • 표현식을 대응하는 집합체 요소 e의 타입으로 바꾸는 암시적 변환 시퀀스를 만들 수 없고, e 자체가 요소를 하나 이상 가진 집합체라면 중괄호 생략(brace elision)이 적용돼요. 이 경우 이후의 초기화 절들은 e의 요소들에 귀속(appertain)돼요. 아래에서 자세히 볼게요. 집합체 요소 e는 그 하위 집합체 요소들(또는 재귀적으로 그 요소들의 집합체 요소들)에 귀속된 초기화 절들로 이뤄진 중괄호 감싼 초기화 리스트로부터, 등장 순서대로 복사 초기화돼요.
  • 초기화 절이 중첩 초기화 리스트(표현식이 아님)라면, 대응하는 집합체 요소는 그 절로부터 복사 리스트 초기화(copy-list-initialize)돼요.
struct A
{
    int x;

    struct B
    {
        int i;
        int j;
    } b;
} a = {1, {2, 3}}; // "a.x"는 1, "a.b.i"는 2, "a.b.j"는 3으로 초기화
  • 비공용체(non-union) 집합체에서, 명시적으로 초기화되지 않은 각 집합체 요소는 다음과 같이 초기화돼요.
    • 그 외의 경우 요소는 참조여서는 안 되고, 빈 초기화 리스트로 복사 초기화돼요.
  • 공용체 집합체에서 초기화 리스트가 비어 있다면:
    • 그 외의 경우 공용체의 첫 번째 멤버(있으면)가 빈 초기화 리스트로 복사 초기화돼요.

어쨌든 집합체 요소들의 초기화는 요소 순서대로 평가돼요. 즉 특정 요소와 관련된 모든 값 계산과 부수 효과는, 순서상 그 뒤에 오는 어떤 요소보다 먼저 sequencing돼요.

struct S { int a; const char* b; int c; int d = b[a]; };
S ss = { 1, "asdf" }; // "ss.a"는 1로
                      // "ss.b"는 "asdf"로
                      // "ss.c"는 int{} (즉 0)로
                      // "ss.d"는 ss.b[ss.a] (즉 's')로 초기화

객체가 크기를 모르는 배열(unknown size)이라면 배열의 크기는 명시적으로 초기화된 요소의 개수(0이면 안 됨)예요. 이 경우 객체는 비정적 데이터 멤버가 될 수 없어요. 멤버는 완전 타입이어야 하니까요.

int x[] = {1, 3, 5}; // x는 요소 3개

struct Y { int i, j, k; };

Y a[] = {1, 2, 3, 4, 5, 6};
X b[2] = {{1, 2, 3}, {4, 5, 6}}; // "a"와 "b"는 같은 타입·값

int z[] = {}; // Error: 요소가 없는 배열은 선언할 수 없음

struct S { int y[] = {0}; }; // Error: 미완성 타입의 비정적 데이터 멤버

객체가 공용체라면 명시적으로 초기화되는 요소는 많아야 하나예요.

union U { int a; const char* b; };

U a = {1};                   // OK: 멤버 "a"를 명시적으로 초기화
U b = {0, "asdf"};           // Error: 두 멤버를 명시적으로 초기화
U c = {"asdf"};              // Error: int는 "asdf"로 초기화 불가

// C++20 지시된 초기화 리스트
U d = {.b = "asdf"};         // OK: 첫 멤버가 아닌 멤버도 명시적으로 초기화 가능
U e = {.a = 1, .b = "asdf"}; // Error: 두 멤버를 명시적으로 초기화

정의

집합체 (Aggregate)

집합체는 다음 타입 중 하나예요.

  • 배열 타입
  • 다음을 갖는 클래스 타입
    • private/protected 직접 비정적 데이터 멤버가 없음
    • 가상 멤버 함수가 없음

요소 (Element)

집합체의 요소는 다음과 같아요.

  • 배열의 경우, 첨자가 커지는 순서의 배열 요소

귀속 (Appertainment)

중괄호 감싼 초기화 리스트의 각 초기화 절은, 초기화되는 집합체의 요소나 그 하위 집합체 중 하나의 요소에 '귀속'된다고 말해요.

초기화 절의 시퀀스와, 초기화되는 집합체의 요소 시퀀스(아래에 설명한 대로 수정될 수 있는)를 생각해 볼게요.

  • 각 초기화 절에 대해 다음 조건 중 하나를 만족하면 그 절은 대응하는 집합체 요소 elem에 귀속돼요.
    • elem이 집합체가 아님.
    • 초기화 절이 {.로 시작함.
    • 초기화 절이 표현식이고, 그 표현식을 elem의 타입으로 바꾸는 암시적 변환 시퀀스를 만들 수 있음.
    • elem이 그 자체로는 집합체 요소가 없는 집합체임.
  • 그 외에는 elem이 집합체이고, 그 하위 집합체가 집합체 요소 목록에서 자신의 집합체 요소들의 시퀀스로 교체되며, 귀속 분석은 그 첫 번째 요소와 같은 초기화 절로 재개돼요. 즉 이 규칙이 집합체의 하위 집합체들에 재귀적으로 적용돼요.

모든 초기화 절이 소진되면 분석이 끝나요. 집합체나 하위 집합체의 어떤 요소에도 귀속되지 않는 초기화 절이 남으면 프로그램은 ill-formed예요.

struct S1 { int a, b; };
struct S2 { S1 s, t; };

// "x"의 각 하위 집합체는 {로 시작하는 초기화 절에 귀속
S2 x[2] =
{
    // "x[0]"에 귀속
    {
        {1, 2}, // "x[0].s"에 귀속
        {3, 4}  // "x[0].t"에 귀속
    },
    // "x[1]"에 귀속
    {
        {5, 6}, // "x[1].s"에 귀속
        {7, 8}  // "x[1].t"에 귀속
    }
};

// "x"와 "y"는 같은 값 (아래 참고)
S2 y[2] = {1, 2, 3, 4, 5, 6, 7, 8};

// "y"의 귀속 분석 과정:
// 1. 집합체 요소 시퀀스를 (x[0], x[1]), 초기화 절 시퀀스를 (1, 2, 3, 4, 5, 6, 7, 8)로 초기화.
// 2. 각 시퀀스의 첫 요소부터 시작해, 1이 x[0]에 귀속되는지 확인:
//    · x[0]은 집합체.
//    · 1은 {.로 시작하지 않음.
//    · 1은 표현식이지만 S2로 암시적으로 변환될 수 없음.
//    · x[0]에는 집합체 요소들이 있음.
// 3. 1은 x[0]에 귀속할 수 없으므로 x[0]은 x[0].s와 x[0].t로 교체되고,
//    집합체 요소 시퀀스는 (x[0].s, x[0].t, x[1])이 됨.
// 4. 귀속 확인을 재개하지만 1은 x[0].s에도 귀속할 수 없음.
// 5. 집합체 요소 시퀀스는 이제 (x[0].s.a, x[0].s.b, x[0].t, x[1])이 됨.
// 6. 귀속 확인을 다시 재개:
//    1은 x[0].s.a에, 2는 x[0].s.b에 귀속.
// 7. 나머지 귀속 분석도 비슷하게 진행.

char cv[4] = {'a', 's', 'd', 'f', 0}; // Error: 초기화 절이 너무 많음

문자 배열

일반 문자 타입(char, signed char, unsigned char)의 배열, char8_t(C++20부터), char16_t, char32_t(C++11부터), wchar_t 배열은 각각 일반 문자열 리터럴, UTF-8 문자열 리터럴(C++20부터), UTF-16 문자열 리터럴, UTF-32 문자열 리터럴(C++11부터), 와이드 문자열 리터럴로 초기화할 수 있어요(선택적으로 중괄호로 감쌀 수 있어요). 추가로 charunsigned char 배열은 UTF-8 문자열 리터럴로 초기화할 수 있어요.(C++20부터) 문자열 리터럴의 연속된 문자(암시적 종료 널 문자 포함)가 배열 요소를 초기화하고, 필요한 경우 소스·목적지 값에 대한 정수 변환이 적용돼요.(C++20부터) 배열 크기가 지정됐고 문자열 리터럴의 문자 수보다 크다면 나머지 문자는 0으로 초기화돼요.

char a[] = "abc";
// char a[4] = {'a', 'b', 'c', '\0'}; 과 동등

//  unsigned char b[3] = "abc"; // Error: 초기화 문자열이 너무 김
unsigned char b[5]{"abc"};
// unsigned char b[5] = {'a', 'b', 'c', '\0', '\0'}; 과 동등

wchar_t c[] = {L"кошка"}; // 중괄호는 선택
// wchar_t c[6] = {L'к', L'о', L'ш', L'к', L'а', L'\0'}; 과 동등

주의

집합체 클래스나 배열은 비집합체 public 기반(C++17부터), 멤버, 요소를 포함할 수 있어요. 그것들은 위에서 설명한 대로 초기화돼요(예: 대응하는 초기화 절로부터의 복사 초기화).

C++11 이전까지는 집합체 초기화에서 축소 변환이 허용됐지만, 이제는 더 이상 허용되지 않아요.

C++11 이전까지는 집합체 초기화가 변수 정의에서만 쓸 수 있었고, 문법 제약 때문에 생성자 초기화 리스트, new 표현식, 임시 객체 생성에는 쓸 수 없었어요.

C 언어에서는 문자열 리터럴 크기보다 하나 작은 문자 배열을 문자열 리터럴로 초기화할 수 있어요. 결과 배열은 널로 끝나지 않죠. C++에서는 이게 허용되지 않아요.

예시

#include <array>
#include <cstdio>
#include <string>

struct S
{
    int x;
    
    struct Foo
    {
        int i;
        int j;
        int a[3];
    } b;
};

int main()
{
    S s1 = {1, {2, 3, {4, 5, 6}}};
    S s2 = {1, 2, 3, 4, 5, 6};  // 같은 일이지만 중괄호 생략 사용
    S s3{1, {2, 3, {4, 5, 6}}}; // 같은 일, 직접 리스트 초기화 문법
    S s4{1, 2, 3, 4, 5, 6}; // CWG 1270까지는 error:
                            // 중괄호 생략은 등호와 함께만 허용됨

    int ar[] = {1, 2, 3}; // ar은 int[3]
//  char cr[3] = {'a', 'b', 'c', 'd'}; // 초기화 절이 너무 많음
    char cr[3] = {'a'}; // {'a', '\0', '\0'}로 초기화

    int ar2d1[2][2] = {{1, 2}, {3, 4}}; // 완전히 중괄호 감싼 2D 배열: {1, 2}
                                        //                              {3, 4}
    int ar2d2[2][2] = {1, 2, 3, 4}; // 중괄호 생략: {1, 2}
                                    //              {3, 4}
    int ar2d3[2][2] = {{1}, {2}}; // 첫 열만: {1, 0}
                                  //          {2, 0}

    std::array<int, 3> std_ar2{{1, 2, 3}};  // std::array는 집합체
    std::array<int, 3> std_ar1 = {1, 2, 3}; // 중괄호 생략 가능

//  int ai[] = {1, 2.0}; // double에서 int로의 축소 변환:
                         // C++11에서는 error, C++03에서는 허용

    std::string ars[] = {std::string("one"), // 복사 초기화
                         "two",              // 변환 후 복사 초기화
                         {'t', 'h', 'r', 'e', 'e'}}; // 리스트 초기화
    union U
    {
        int a;
        const char* b;
    };
    U u1 = {1};         // OK, 공용체의 첫 멤버
//  U u2 = {0, "asdf"}; // Error: 공용체에는 초기화가 너무 많음
//  U u3 = {"asdf"};    // Error: int로의 잘못된 변환
}

// 집합체
struct Base1 { int b1, b2 = 42; };

// 비집합체
struct Base2
{
    Base2() : b3(42) {} // 생성자가 이 클래스를 비집합체로 만듦
    
    int b3;
};

// C++17의 집합체
struct Derived : Base1, Base2 { int d; };

Derived d1{{1, 2}, {}, 4}; // d1.b1 = 1, d1.b2 = 2,  d1.b3 = 42, d1.d = 4
Derived d2{{}, {}, 4};     // d2.b1 = 0, d2.b2 = 42, d2.b3 = 42, d2.d = 4

더 알아보기

  • 초기화 전반(기본·값·복사·직접·리스트 초기화)을 묶어서 보면 흐름이 잡혀요.
  • 복사 생략(copy elision)과 함께 보면 반환 값 초기화까지 이어져요.
  • cppreference의 집합체 초기화 원문에서 결함 보고 기록을 더 볼 수 있어요.