Even Idiots Can Make Game

중괄호 초기화의 모든 것

Date/Lastmod
03-13/25
Section
DEV
Categories
C++

#1 개요: 같은 중괄호라고 다 같은 게 아니다

중괄호 초기화는 중괄호 {}를 사용하는 모든 초기화 방식을 포괄하는 용어이며, 표준에서는 이를 목록 초기화(List Initialization) 라고 부른다.

int x{10};
int y = {10};
std::vector<int> v{1, 2, 3};

이처럼 다양한 타입의 객체들을 일관성 있게 초기화 할 수 있다는 것이 큰 장점이다.

그러나, 같은 {}를 사용한 초기화여도 작동 방식이 조금씩 다르므로 주의가 필요하다. “중괄호를 사용하는 초기화"가 여러 종류인 것이고, 이를 포괄하여 “중괄호 초기화"라고 부르는 것이라고 보는 것이 타당하다.

#1 왜 써야할까?: Most Vexing Parse 해결

int main() {
  Foo foo(); // ??
  Foo ffoo(Bar()); // ????
}

Foo foo()Foo의 기본 생성자를 호출할 의도로 작성하였겠으나, 컴파일러는 이를 반환값이 Foo이고 매개변수가 없는 함수 원형 선언으로 해석한다.

Foo ffo(Bar())는 더 기묘한 예인데, Bar형 임시 객체를 전달하여 걸맞는 Foo의 생성자를 호출할 의도로 작성되었겠지만, 컴파일러는 이를 반환 형식이 Foo이고, 매개 변수로는 반환값이 Bar이고 매개 변수로 아무것도 받지 않는 익명 함수 포인터를 받는 함수 선언과 구별할 수 없다고 판단한다.

이를 C++의 Most Vexing Parse 문제라고 하며, 이 문제의 해결책 중 하나가 중괄호 초기화이다.

다음처럼 중괄호 초기화를 사용하면 명확한 의도를 전달할 수 있다.

int main() {
  Foo foo{}; // 명확해!
  Foo ffo{Bar{}}; // 너무 명확해!!
}

#1 종류

#2 직접 목록 초기화 (Direct List Initialization)

int a{10};
std::vector<int> v{1, 2, 3};
Foo foo{10, 10};

직접 목록 초기화에서는 명시적으로 {}와 인자를 전달하여 개체를 초기화한다.

전달된 인자에 맞는 가장 적절한 생성자를 찾아 호출하려고 한다:

이 방식으로 explicit 생성자도 호출이 가능하다.

#3 std::initializer_list 우선순위

std::initializer_list를 받는 생성자를 가장 먼저 우선한다는 것을 주의해야 한다.

std::vector<int> vec_0(5, 10); // 크기 5, 모든 요소 10
std::vector<int> vec_1{5, 10}; // 요소가 5, 10

std::vector에는 std::initializer_list를 받는 생성자가 있으므로 중괄호로 인자를 전달하면 해당 생성자가 최우선적으로 호출되어, 크기와 기본값이 아닌 요소들의 목록으로 해석된다.

#2 복사 리스트 초기화 (Copy List Initialization)

int b = {20};
std::vector<int> v = {1, 2, 3};
Foo foo = {10, 10};

= 연산자를 이용한 중괄호 초기화이다.
explicit 생성자는 호출되지 않으며, 암시적 변환이 허용된다.

실제로는 임시 객체를 생성한 후 복사/이동하는 과정으로 설계되었으나, C++17 이후 이런 상황에서 복사 생략이 의무화된 관계로 복사가 일어나지는 않는다.

#2 값 초기화 (Value Initialization)

int c{};
std::string s{}; // 빈 문자열
Foo foo{}; // 기본 생성자

내장형(Primitive) 타입이면 0으로 초기화된다.
클래스 타입이면 기본 생성자를 호출한다.

#2 집계 초기화 (Aggergate Initialization)

struct Foo { int x, y; };
Foo foo{10, 20};
int arr[] {1, 2, 3};
std::array<int, 3> s_arr {1, 2, 3}; // 주의!

집계체(Aggergate) 에 대해 각 멤버들을 중괄호 {}로 직접 초기화 할 수 있다.
배열 또한 집계체이므로, 같은 방식으로 초기화 할 수 있다.

#3 집계체?

C++ 표준에서 집계체는 다음 조건을 만족하는 클래스, 구조체, 배열이다.

POD와 비슷한 개념이지만, POD는 집계체보다 조금 더 엄격한 개념이다.
POD는 모든 멤버가 POD여야 하고, 멤버 함수 또한 없어야 한다.
모든 POD는 집계체이나, 집계체라고 POD는 아니다.

#3 std::array vs std::vector

std::array의 구현을 단순화해보면 아래와 같다. (MSVC 기준)

template <class T, std::size_t N>
class array {
  T elems[N];
};

std::array는 C 스타일 배열을 안전하게 감싸는 역할만 수행하는 집계체이다. 즉, C 스타일 배열과 동일하게 초기화될 수 있도록 설계되었다.

std::array에는 사용자 정의 생성자가 존재하지 않으며 당연히 std::vector처럼 std::initializer_list를 받는 생성자도 존재하지 않는다.

int arr[] {1, 2, 3};                // 집계 초기화
std::array<int, 3> s_arr {1, 2, 3}; // 집계 초기화
std::vector<int> vec {1, 2, 3};     // 직접 목록 초기화

때문에 위 두 초기화는 비슷해 보이지만, 내부적으로는 꽤 다르게 작동한다고 볼 수 있다.

[참고] 사실 std::array의 초기화 형태는 중괄호 생략에 의한 형태이다. 사실 내부에 C 스타일 배열을 가지고 있으므로, {{1, 2, 3}}과 같은 형태가 정확하다. 하지만 중괄호 생략에 의해 {1, 2, 3}을 허용한다.

#1 특징

#2 축소 변환 방지

중괄호 초기화의 중요한 특징으로, 축소 변환을 방지한다.

double d = 3.14;
int x1 = d;
int x2 {d}; // 컴파일 오류: double -> int 금지

그러나, 작은 형식으로 변환하는 축소 변환만 방지함을 유의한다.

char c{65}; // int -> char 허용 안됨
int i{'c'}; // char -> int 허용됨

#2 중괄호 생략 가능

중첩 배열을 초기화할 때 내부 중괄호를 생략할 수 있다.

int mat[2][2] { 1, 2, 3, 4 }; // {{1, 2}, {3, 4}} 와 같다

이런게 가능한 줄은 꿈에도 몰랐다.

comments powered by Disqus