리스트 초기화
리스트 초기화 (List-initialization)
리스트 초기화(list-initialization) 는 중괄호로 감싼 이니셜라이저 목록(brace-enclosed initializer list)에서 객체를 초기화하는 방식이에요.
출처: cppreference
본문
문법 (Syntax)
직접 리스트 초기화 (Direct-list-initialization)
T object { arg1, arg2, ... }; (1)
T object {.des1 = arg1 , .des2 { arg2 } ... }; (1, C++20부터)
T { arg1, arg2, ... } (2)
T {.des1 = arg1 , .des2 { arg2 } ... } (2, C++20부터)
new T { arg1, arg2, ... } (3)
new T {.des1 = arg1 , .des2 { arg2 } ... } (3, C++20부터)
Class { T member { arg1, arg2, ... }; }; (4)
Class { T member {.des1 = arg1 , .des2 { arg2 } ... }; }; (4, C++20부터)
Class::Class() : member { arg1, arg2, ... } {... (5)
Class::Class() : member {.des1 = arg1 , .des2 { arg2 } ...} {... (5, C++20부터)
복사 리스트 초기화 (Copy-list-initialization)
T object = { arg1, arg2, ... }; (6)
T object = {.des1 = arg1 , .des2 { arg2 } ... }; (6, C++20부터)
function ({ arg1, arg2, ... }) (7)
function ({.des1 = arg1 , .des2 { arg2 } ... }) (7, C++20부터)
return { arg1, arg2, ... }; (8)
return {.des1 = arg1 , .des2 { arg2 } ... }; (8, C++20부터)
object [{ arg1, arg2, ... }] (9)
object [{.des1 = arg1 , .des2 { arg2 } ... }] (9, C++20부터)
object = { arg1, arg2, ... } (10)
object = {.des1 = arg1 , .des2 { arg2 } ... } (10, C++20부터)
U ({ arg1, arg2, ... }) (11)
U ({.des1 = arg1 , .des2 { arg2 } ... }) (11, C++20부터)
Class { T member = { arg1, arg2, ... }; }; (12)
Class { T member = {.des1 = arg1 , .des2 { arg2 } ... }; }; (12, C++20부터)
리스트 초기화는 다음 상황에서 수행돼요.
직접 리스트 초기화 (명시적 생성자와 비명시적 생성자 모두 고려):
- 중괄호로 감싼 이니셜라이저 목록으로 명명된 변수 초기화
- 중괄호로 감싼 이니셜라이저 목록으로 이름 없는 임시 객체 초기화
new표현식으로 동적 저장 기간 객체 초기화 (이니셜라이저가 중괄호 목록일 때)- 등호를 사용하지 않는 비정적 데이터 멤버 이니셜라이저
- 생성자의 멤버 이니셜라이저 목록에서 중괄호 목록을 사용할 때
복사 리스트 초기화 (명시적 생성자와 비명시적 생성자를 모두 고려하지만, 오직 비명시적 생성자만 호출될 수 있음):
- 등호 뒤에서 중괄호 목록으로 명명된 변수 초기화
- 함수 호출 표현식에서 중괄호 목록을 인자로 쓰고 리스트 초기화가 함수 매개변수를 초기화할 때
return문에서 중괄호 목록을 반환 표현식으로 쓰고 리스트 초기화가 반환된 객체를 초기화할 때- 사용자 정의
operator[]가 있는 첨자 표현식에서 리스트 초기화가 오버로드된 연산자의 매개변수를 초기화할 때 - 대입 표현식에서 리스트 초기화가 오버로드된 연산자의 매개변수를 초기화할 때
- 함수형 캐스트 표현식이나 다른 생성자 호출에서, 생성자 인자 대신 중괄호 목록을 쓸 때 (복사 리스트 초기화가 생성자의 매개변수를 초기화해요. 주의: 이 예의 타입
U는 리스트 초기화되는 타입이 아니라U의 생성자 매개변수가 초기화되는 거예요) - 등호를 사용하는 비정적 데이터 멤버 이니셜라이저
설명 (Explanation)
(가능하면 cv-한정된) 타입 T의 객체 리스트 초기화의 효과는 이래요.
- 중괄호 목록에 지정된 이니셜라이저 목록(designated initializer list)이 있고
T가 참조 타입이 아니면,T는 집계 클래스여야 해요. 지정된 목록의 지정자(designator)에 있는 순서 있는 식별자들은T의 직접 비정적 데이터 멤버들의 순서 있는 식별자들의 부분 수열을 이뤄야 해요. 집계 초기화가 수행돼요. (C++20부터) T가 집계 클래스이고, 지정된 목록이 없는 중괄호 목록(C++20부터)이 같은 타입 또는 파생 타입의 단일 이니셜라이저 절을 가지면, 객체는 그 이니셜라이저 절로부터 초기화돼요 (복사 리스트 초기화에서는 복사 초기화로, 직접 리스트 초기화에서는 직접 초기화로).- 그 외에
T가 문자 배열이고 중괄호 목록이 적절한 타입의 문자열 리터럴인 단일 이니셜라이저 절을 가지면, 배열은 평소대로 문자열 리터럴에서 초기화돼요. - 그 외에
T가 집계 타입이면 집계 초기화가 수행돼요. - 그 외에 중괄호 목록이 비어 있고
T가 기본 생성자를 가진 클래스 타입이면 값 초기화(value-initialization)가 수행돼요. - 그 외에
T가std::initializer_list의 특수화면 객체는 아래에 설명된 대로 초기화돼요. - 그 외에
T가 클래스 타입이면T의 생성자들이 두 단계로 고려돼요.std::initializer_list를 유일한 인자로 받거나, 나머지 인자에 기본 값이 있으면 첫 인자로 받는 모든 생성자들을 조사해, 타입std::initializer_list의 단일 인자에 대해 오버로드 결정으로 대응을 찾아요.- 이전 단계가 대응을 만들지 못하면,
T의 모든 생성자들이 중괄호 목록의 이니셜라이저 절들로 구성된 인자 집합에 대해 오버로드 결정에 참여해요. 단 비축소(non-narrowing) 변환만 허용돼요. 이 단계에서 복사 리스트 초기화에 대한 최상의 대응으로 명시적 생성자가 나오면 컴파일이 실패해요 (참고: 단순 복사 초기화에서는 명시적 생성자가 전혀 고려되지 않아요).
- 그 외에
T가 고정 기반 타입U를 가진 열거 타입이고 중괄호 목록이 이니셜라이저v하나만 가지며 다음 조건을 모두 만족하면, 열거형은v를U로 변환한 결과로 초기화돼요. (C++17부터)- 초기화가 직접 리스트 초기화이다.
v가 스칼라 타입이다.v가U로 암시적으로 변환될 수 있다.v에서U로의 변환이 비축소이다.
- 그 외에 (
T가 클래스 타입이 아니면) 중괄호 목록이 이니셜라이저 절 하나만 가지고,T가 참조 타입이 아니거나 이니셜라이저 절 타입과 같거나 그 기반 클래스인 참조 타입이면,T는 직접 초기화(직접 리스트 초기화) 또는 복사 초기화(복사 리스트 초기화)되는데, 단 축소 변환은 허용되지 않아요. - 그 외에
T가 이니셜라이저 절 타입과 호환되지 않는 참조 타입이면:- (C++17까지)
T가 가리키는 타입의 prvalue 임시 객체가 복사 리스트 초기화되고, 참조는 그 임시 객체에 바인딩돼요 (참조가 비-const lvalue 참조면 실패해요). - (C++17부터) prvalue가 생성돼요. 그 prvalue는 복사 리스트 초기화로 자신의 결과 객체를 초기화해요. 그런 다음 그 prvalue가 참조를 직접 초기화하는 데 쓰여요 (참조가 비-const lvalue 참조면 실패해요). 임시 객체의 타입은
T가 가리키는 타입인데,T가 "U 의 불명확한 결계 배열에 대한 참조"이면 임시 객체의 타입은 선언U x[] H에서x의 타입이에요 (H 는 이니셜라이저 목록, C++20부터).
- (C++17까지)
- 그 외에 중괄호 목록에 이니셜라이저 절이 없으면
T는 값 초기화돼요.
std::initializer_list 리스트 초기화 (List-initializing std::initializer_list)
타입 std::initializer_list<E>의 객체는, 컴파일러가 타입 "array of N const E"의 prvalue를 생성하고 구체화한 것처럼 이니셜라이저 목록에서 만들어져요. 여기서 N은 이니셜라이저 목록의 이니셜라이저 절 수예요. 이걸 이니셜라이저 목록의 백킹 배열(backing array) 이라고 불러요.
백킹 배열의 각 요소는 이니셜라이저 목록의 대응하는 이니셜라이저 절로 복사 초기화되고, std::initializer_list<E> 객체는 그 배열을 가리키도록 만들어져요. 복사에 선택된 생성자나 변환 함수는 이니셜라이저 목록의 맥락에서 접근 가능해야 해요. 요소를 초기화하는 데 축소 변환이 필요하면 프로그램은 규칙 위반이에요.
백킹 배열은 다른 임시 객체와 같은 수명을 가져요. 단 std::initializer_list 객체를 백킹 배열에서 초기화하면, 참조를 임시 객체에 바인딩하는 것과 똑같이 배열의 수명을 연장해요.
void f(std::initializer_list<double> il);
void g(float x)
{
f({1, x, 3});
}
void h()
{
f({1, 2, 3});
}
struct A { mutable int i; };
void q(std::initializer_list<A>);
void r()
{
q({A{1}, A{2}, A{3}});
}
// The initialization above will be implemented in a way roughly equivalent to below,
// assuming that the compiler can construct an initializer_list object with a pair of
// pointers, and with the understanding that `__b` does not outlive the call to `f`.
void g(float x)
{
const double __a[3] = {double{1}, double{x}, double{3}}; // backing array
f(std::initializer_list<double>(__a, __a + 3));
}
void h()
{
static constexpr double __b[3] =
{double{1}, double{2}, double{3}}; // backing array
f(std::initializer_list<double>(__b, __b + 3));
}
void r()
{
const A __c[3] = {A{1}, A{2}, A{3}}; // backing array
q(std::initializer_list<A>(__c, __c + 3));
}
모든 백킹 배열이 서로 구별되는지(즉 겹치지 않는 객체에 저장되는지)는 지정되지 않아요.
bool fun(std::initializer_list<int> il1, std::initializer_list<int> il2)
{
return il2.begin() == il1.begin() + 1;
}
bool overlapping = fun({1, 2, 3}, {2, 3, 4}); // the result is unspecified:
// the back arrays can share
// storage within {1, 2, 3, 4}
축소 변환 (Narrowing conversions)
리스트 초기화는 다음을 금지해서 허용되는 암시적 변환을 제한해요.
- 부동소수점 타입에서 정수 타입으로의 변환
- 부동소수점 타입
T에서 변환 순위가T보다 크지도 같지도 않은 다른 부동소수점 타입으로의 변환 (단 변환 결과가 상수 표현식이고 변환된 값이 유한하며 오버플로되지 않거나, 변환 전후 값이 모두 유한하지 않은 경우는 예외) - 정수 타입에서 부동소수점 타입으로의 변환 (단 소스가 상수 표현식이고 그 값을 목적지 타입에 정확히 저장할 수 있는 경우는 예외)
- 정수 또는 unscoped 열거 타입에서 원본의 모든 값을 표현할 수 없는 정수 타입으로의 변환 (단 소스가 비트 필드로 그 폭
w가 타입의 폭보다 작고 목적지 타입이 폭w의 가상 확장 정수 타입의 모든 값과 원본 타입과 같은 부호를 표현할 수 있는 경우, 또는 소스가 상수 표현식이고 그 값을 목적지 타입에 정확히 저장할 수 있는 경우는 예외) - 포인터 타입 또는 멤버 포인터 타입에서
bool로의 변환
참고 (Notes)
- 각 이니셜라이저 절은 중괄호 목록에서 그 뒤에 오는 어떤 이니셜라이저 절보다도 앞서 이어져요(sequenced). 이는 함수 호출 표현식의 인자들과 대조되는데, 인자들은 서로 순서가 없거나(C++17까지) 순서가 결정되지 않게 이어져요(C++17부터).
- 중괄호로 감싼 이니셜라이저 목록은 표현식이 아니므로 타입이 없어요. 예를 들어
decltype({1, 2})는 규칙 위반이에요. 타입이 없다는 것은 템플릿 타입 추론이 중괄호 목록에 맞는 타입을 추론할 수 없다는 뜻이에요. 그래서template<class T> void f(T);라는 선언이 있을 때f({1, 2, 3})는 규칙 위반이에요. 그러나 템플릿 매개변수가 다른 방식으로 추론되면 가능해요.std::vector<int> v(std::istream_iterator<int>(std::cin), {})가 그 예인데, 반복자 타입은 첫 인자로 추론되면서 두 번째 매개변수 위치에도 쓰여요. 예외가 하나 있는데,auto키워드를 쓰는 타입 추론은 복사 리스트 초기화에서 어떤 중괄호 목록이든std::initializer_list로 추론해요. - 중괄호 목록은 타입이 없으므로, 오버로드된 함수 호출의 인자로 쓰일 때 특별한 오버로드 결정 규칙이 적용돼요.
- 집계는 같은 타입의 단일 이니셜라이저 절로 이루어진 중괄호 목록에서 직접 복사/이동 초기화하지만, 비집계는
std::initializer_list생성자를 먼저 고려해요.
struct X {}; // aggregate
struct Q // non-aggregate
{
Q() = default;
Q(Q const&) = default;
Q(std::initializer_list<Q>) {}
};
int main()
{
X x;
X x2 = X{x}; // copy-constructor (not aggregate initialization)
Q q;
Q q2 = Q{q}; // initializer-list constructor (not copy constructor)
}
- 어떤 컴파일러(예: gcc 10)는 C++20 모드에서만 포인터 또는 멤버 포인터에서
bool로의 변환을 축소로 간주해요.
기능 테스트 매크로: __cpp_initializer_lists(200806L, C++11, 리스트 초기화와 std::initializer_list).
예제 (Example)
#include <iostream>
#include <map>
#include <string>
#include <vector>
struct Foo
{
std::vector<int> mem = {1, 2, 3}; // list-initialization of a non-static member
std::vector<int> mem2;
Foo() : mem2{-1, -2, -3} {} // list-initialization of a member in constructor
};
std::pair<std::string, std::string> f(std::pair<std::string, std::string> p)
{
return {p.second, p.first}; // list-initialization in return statement
}
int main()
{
int n0{}; // value-initialization (to zero)
int n1{1}; // direct-list-initialization
std::string s1{'a', 'b', 'c', 'd'}; // initializer-list constructor call
std::string s2{s1, 2, 2}; // regular constructor call
std::string s3{0x61, 'a'}; // initializer-list ctor is preferred to (int, char)
int n2 = {1}; // copy-list-initialization
double d = double{1.2}; // list-initialization of a prvalue, then copy-init
auto s4 = std::string{"HelloWorld"}; // same as above, no temporary
// created since C++17
std::map<int, std::string> m = // nested list-initialization
{
{1, "a"},
{2, {'a', 'b', 'c'}},
{3, s1}
};
std::cout << f({"hello", "world"}).first // list-initialization in function call
<< '\n';
const int (&ar)[2] = {1, 2}; // binds an lvalue reference to a temporary array
int&& r1 = {1}; // binds an rvalue reference to a temporary int
// int& r2 = {2}; // error: cannot bind rvalue to a non-const lvalue ref
// int bad{1.0}; // error: narrowing conversion
unsigned char uc1{10}; // okay
// unsigned char uc2{-1}; // error: narrowing conversion
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); // has effect of [[maybe_unused]]
}
출력:
world
0 1 1
abcd cd aa
1 a
2 abc
3 abcd
1 2 3 -1 -2 -3
결함 보고 (Defect reports)
동작 변경 결함 보고로 주요하게 조정된 내용은 이래요. 참조를 단일 이니셜라이저 절로 리스트 초기화할 때 유효하면 그 절에 직접 바인딩하도록 함(CWG 1288), 백킹 배열 수명을 다른 임시 객체와 같게 지정함(CWG 1290), {} 초기화에서 집계 초기화를 먼저 고려하도록 함(CWG 1324), 백킹 배열 타입에 const 추가(CWG 1418), 집계·문자 배열의 같은 타입 초기화 허용 및 단일 절 목록의 직접 초기화(CWG 1467), 참조 리스트 초기화의 임시 객체가 복사/직접 중 무엇으로 초기화되는지 명확화(CWG 1494, CWG 2267), {X}에서의 initializer-list 생성자 우선(CWG 2137), 열거형의 비스칼라 값 리스트 초기화 금지(CWG 2252), enum 직접 리스트 초기화의 소스 타입 제한(CWG 2374), 비트 필드 승격이 축소가 아니게 됨(CWG 2627), 지정 이니셜라이저 목록으로 집계 클래스 참조 초기화 허용(CWG 2713), 리스트 초기화가 최상위 cv-한정을 무시하도록 함(CWG 2830), 오버플로하는 부동소수점 변환이 축소가 되게 함(CWG 2864), 포인터/멤버 포인터에서 bool 변환을 축소로 간주함(P1957R2), 수명이 겹치는 백킹 배열이 겹칠 수 있게 함(P2752R3) 등이에요.