list_initialization — 목록 초기화

list_initialization — 목록 초기화 (List-initialization)

**목록 초기화(list-initialization)**는 중괄호로 감싼 초기화 목록(brace-enclosed initializer list)으로 객체를 초기화하는 방식이에요. T obj{arg1, arg2} 형태로 초기화할 때 적용돼요.

중괄호 초기화는 수축 변환(narrowing conversion)을 금지해 타입 안전성을 높이고, std::initializer_list 생성자와 자연스럽게 연결돼요. 그래서 컨테이너를 쉽게 초기화하는 데 유용해요.

출처: cppreference

본문

목록 초기화는 중괄호로 감싼 초기화 목록으로 객체를 초기화해요.

직접 목록 초기화 (Direct-list-initialization) 문법

T object { arg1, arg2, ... };
T { arg1, arg2, ... }
new T { arg1, arg2, ... }
Class { T member { arg1, arg2, ... }; };
Class::Class() : member { arg1, arg2, ... } {...}

직접 목록 초기화에서는 explicit과 non-explicit 생성자 모두 고려돼요.

복사 목록 초기화 (Copy-list-initialization) 문법

T object = { arg1, arg2, ... };
function({ arg1, arg2, ... })
return { arg1, arg2, ... };
object[{ arg1, arg2, ... }]
object = { arg1, arg2, ... }
U({ arg1, arg2, ... })
Class { T member = { arg1, arg2, ... }; };

복사 목록 초기화에서는 explicit과 non-explicit 생성자 모두 고려되지만, 호출될 수 있는 것은 non-explicit 생성자뿐이에요.

목록 초기화는 다음 상황에서 수행돼요. (1) 이름 있는 변수를 중괄호 초기화 목록으로, (2) 이름 없는 임시를 중괄호 초기화 목록으로, (3) 초기화자가 중괄호 목록인 new 표현식으로 동적 객체를, (4) 등호를 안 쓰는 비정적 데이터 멤버 초기화자에서, (5) 생성자의 멤버 초기화 목록에서 중괄호 목록을 쓸 때 등 직접 목록 초기화. 등호 뒤 중괄호, 함수 호출 인자, return 문, 첨자·대입 표현식, 함수형 캐스트, 등호를 쓰는 데이터 멤버 초기화자에서는 복사 목록 초기화가 수행돼요.

설명 (Explanation)

(가능하면 cv 한정된) T 타입 객체의 목록 초기화의 효과는 다음과 같아요.

  • 중괄호 초기화 목록이 지정 초기화자 목록(designated initializer list)을 포함하고 T가 참조 타입이 아니면, T는 애그리거트 클래스여야 해요. 지정자의 순서가 T의 직접 비정적 데이터 멤버 순서의 부분 수열이어야 하고, 애그리거트 초기화가 수행돼요.
  • T가 애그리거트 클래스이고 목록에 같은/파생 타입의 단일 초기화 절이 있으면, 그 초기화 절로부터(복사 목록 초기화면 복사 초기화, 직접 목록 초기화면 직접 초기화로) 객체가 초기화돼요.
  • 그 외에 T가 문자 배열이고 목록에 적절한 타입의 문자열 리터럴 단일 초기화 절이 있으면, 그 리터럴로부터 배열이 초기화돼요.
  • 그 외에 T가 애그리거트 타입이면 애그리거트 초기화가 수행돼요.
  • 그 외에 목록이 비어 있고 T가 기본 생성자를 가진 클래스 타입이면 값 초기화가 수행돼요.
  • 그 외에 Tstd::initializer_list의 특수화면, 아래 설명대로 객체가 초기화돼요.
  • 그 외에 T가 클래스 타입이면, 두 단계로 T의 생성자를 고려해요.
    1. std::initializer_list만 인자로 받거나 (나머지 인자가 기본값일 때) 첫 번째 인자로 받는 모든 생성자가 검사되고, 단일 std::initializer_list 인자에 대해 과부하 해석으로 매칭돼요.
    2. 이전 단계에서 매치가 없으면, T의 모든 생성자가 목록의 초기화 절로 이루어진 인자 집합에 대해 과부하 해석에 참여하는데, non-narrowing 변환만 허용돼요. 복사 목록 초기화의 최적 매치로 explicit 생성자가 나오면 컴파일이 실패해요.
  • 그 외에 T가 고정 기반 타입 U를 가진 열거형이고 목록에 단일 초기화자 v가 있으며 여러 조건을 만족하면, vU로 변환한 결과로 열거형을 초기화해요.
  • 그 외에 (비클래스 타입이면) 목록에 단일 초기화 절이 있고, (참조가 아니거나 참조 타입이 초기화 절의 타입과 같거나 그 기반인 경우) 수축 변환을 제외하고 직접/복사 초기화로 T를 초기화해요.
  • 그 외에 목록에 초기화 절이 없으면 T를 값 초기화해요.

std::initializer_list 목록 초기화

std::initializer_list<E> 타입 객체는, 컴파일러가 생성/구체화한 "N개의 const E 배열" 타입의 prvalue(초기화 목록의 백킹 배열(backing array))에서 생성돼요. 백킹 배열의 각 요소는 대응하는 초기화 절로 복사 초기화되고, std::initializer_list<E> 객체는 그 배열을 참조하도록 만들어져요. 요소 초기화에 수축 변환이 필요하면 프로그램은 잘못된 형식이에요.

백킹 배열은 다른 임시 객체와 같은 수명을 가지되, 백킹 배열에서 std::initializer_list 객체를 초기화하면 참조를 임시에 바인딩하는 것처럼 배열의 수명을 연장해요.

수축 변환

목록 초기화는 다음의 수축 변환을 금지해요.

  • 부동 소수점 타입에서 정수 타입으로의 변환
  • 부동 소수점 타입에서 더 높지 않은 순위의 부동 소수점 타입으로의 변환 (상수 표현식으로 유한하고 오버플로우하지 않는 경우 제외)
  • 정수 타입에서 부동 소수점 타입으로의 변환 (상수 표현식으로 정확히 저장될 수 있는 경우 제외)
  • 원래의 모든 값을 표현할 수 없는 정수/무범위 열거형 타입에서 정수 타입으로의 변환
  • 포인터/멤버 포인터 타입에서 bool로의 변환

노트 (Notes)

각 초기화 절은 그 뒤에 오는 초기화 절보다 앞에 순서지워져요. 함수 호출의 인자와는 달라요.

중괄호 초기화 목록은 표현식이 아니므로 타입이 없어요. decltype({1, 2})는 잘못된 형식이에요. 타입이 없다는 것은 템플릿 타입 추론이 중괄호 초기화 목록과 일치하는 타입을 추론할 수 없다는 뜻이에요. 다만 auto 키워드는 복사 목록 초기화에서 중괄호 초기화 목록을 std::initializer_list로 추론하는 특별한 예외가 있어요.

예제 (Example)

#include <iostream>
#include <map>
#include <string>
#include <vector>

struct Foo
{
    std::vector<int> mem = {1, 2, 3}; // 비정적 멤버의 목록 초기화
    std::vector<int> mem2;

    Foo() : mem2{-1, -2, -3} {} // 생성자에서 멤버의 목록 초기화
};

std::pair<std::string, std::string> f(std::pair<std::string, std::string> p)
{
    return {p.second, p.first}; // return 문에서 목록 초기화
}

int main()
{
    int n0{};  // 값 초기화 (0 으로)
    int n1{1}; // 직접 목록 초기화

    std::string s1{'a', 'b', 'c', 'd'}; // initializer-list 생성자 호출
    std::string s2{s1, 2, 2};           // 일반 생성자 호출
    std::string s3{0x61, 'a'}; // (int, char) 보다 initializer-list 생성자 우선

    int n2 = {1}; // 복사 목록 초기화
    double d = double{1.2}; // prvalue 의 목록 초기화, 그 후 복사 초기화
    auto s4 = std::string{"HelloWorld"}; // 위와 동일, C++17 부터 임시 생성 없음

    std::map<int, std::string> m = // 중첩 목록 초기화
    {
        {1, "a"},
        {2, {'a', 'b', 'c'}},
        {3, s1}
    };

    std::cout << f({"hello", "world"}).first // 함수 호출에서 목록 초기화
              << '\n';

    const int (&ar)[2] = {1, 2}; // 임시 배열에 lvalue 참조 바인딩
    int&& r1 = {1}; // 임시 int 에 rvalue 참조 바인딩
//  int& r2 = {2}; // 오류: rvalue 를 비const lvalue 참조에 바인딩 불가

//  int bad{1.0}; // 오류: 수축 변환
    unsigned char uc1{10}; // 정상
//  unsigned char uc2{-1}; // 오류: 수축 변환

    Foo f;

    std::cout << n0 << ' ' << n1 << ' ' << n2 << '\n'
              << s1 << ' ' << s2 << ' ' << s3 << '\n';
    for (auto p : m)
        std::cout << p.first << ' ' << p.second << '\n';
    for (auto n : f.mem)
        std::cout << n << ' ';
    for (auto n : f.mem2)
        std::cout << n << ' ';
    std::cout << '\n';

    [](...){}(d, ar, r1, uc1); // [[maybe_unused]] 효과
}

출력:

world
0 1 1
abcd cd aa
1 a
2 abc
3 abcd
1 2 3 -1 -2 -3

핵심 포인트예요. {1.0}처럼 수축 변환(부동→정수)은 오류가 나고, std::map은 중괄호로 짝의 목록을 만들어 쉽게 초기화할 수 있어요. std::string s3{0x61, 'a'}처럼 initializer-list 생성자가 우선 선택되는 것도 볼 수 있어요.

더 알아보기 (Learn more)

cppreference