25 분 소요

개요

C++11 부터는 개체 생성시 중괄호 초기화를 이용한 방법이 추가되어 일관된 초기화 방법을 제공하고, 초기화 파싱 오류를 개선했습니다.

다음과 같이 기존 괄호(())를 중괄호({})로 대체하여 초기화 할 수 있습니다. 이때 = 사용 여부에 따라 중괄호 직접 초기화중괄호 복사 초기화로 구분합니다.

1
2
3
4
5
int a = 10;

int b(10); // 괄호 초기화
int b_11{10}; // 중괄호 직접 초기화
int c_11 = {10}; // 증괄호 복사 초기화

중괄호 초기화

초기화 파싱 오류에서도 언급했듯 괄호(())를 사용하는 초기화 방법은 컴파일러가 다르게 해석(기본 생성자 호출을 함수 선언으로 오해)할 소지가 있습니다. 그리고, 클래스인지, 배열인지, 구조체 인지에 따라 때로는 괄호를 생략하거나, 괄호를 넣거나, 중괄호를 사용해야 하거나 뒤죽 박죽입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class T {
    int m_A;
    char m_B;
public:
    T() {}
    T(int a, char b) : m_A(a), m_B(b) {}    
};
T obj(); // (△) 비권장. T를 리턴하는 obj 함수 선언

T obj1; // 기본 생성자로 T 개체 생성
T obj2(T()); // (△) 비권장. T 타입의 기본 생성자로 생성한 것을 T obj2에 복사 생성하고 싶지만, T 타입을 리턴하고, T(*)()함수 포인터를 인자로 전달받는 함수 obj2를 선언합니다. 
T obj3 = T(); // T obj(T());와 유사. T()로 기본 생성된 것을 T obj3에 복사 생성. 단 컴파일러 최적화로 1회만 생성될 수 있음
T obj4(10, 'b'); // m_A == 10, m_B == 'b'인 T 개체 생성

T arr[] = {T(), T(10, 'b')}; // T 요소 2개인 배열 생성

struct U {
    int m_A;
    char m_B;
};
U objs = {10, 'b'}; // m_A == 10, m_B == 'b'인 U 개체 생성

C++11 부터는 중괄호 초기화를 제공하여 클래스인지, 배열인지, 구조체인지 구분없이 중괄호({})를 이용하여 일관성 있게 초기화 할 수 있으며, 초기화 파싱 오류도 해결했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class T {
    int m_A;
    char m_B;
public:
    T() {}
    T(int a, char b) : m_A(a), m_B(b) {}    
};

T obj1_11{}; // 기본 생성자로 T 개체 생성
T obj2_11{T{}}; // 기본 생성자인 T()로 생성한 개체를 obj2_11의 복사 생성자로 복사 생성
T obj3_11 = T{}; // T obj2_11{T{}}와 유사. T{}로 기본 생성된 것을 T obj2_11 복사 생성. 단 컴파일러 최적화로 1회만 생성될 수 있음
T obj4_11{10, 'b'}; // T(int a, char b) 생성자 호출. m_A == 10, m_B == 'b'인 T 개체 생성

T arr_11[]{T{}, T{10, 'b'}}; // T 요소 2개인 배열 생성

struct U {
    int m_A;
    char m_B;
};
U objs_11{10, 'b'}; // m_A == 10, m_B == 'b'인 U 개체 생성    

중괄호 직접 초기화 T t{};

T{} 표현은 T()와 같이 생성자를 호출하고, 초기값을 전달합니다.

  1. 기존에는 생성자 호출시 T()와 같이 괄호(())를 사용했는데, 중괄호({})를 사용할 수 있습니다. 특히 중괄호를 사용하면, 기본 생성자 호출이 함수 선언으로 인식되는 초기화 파싱 오류가 말끔히 해결됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     class T {
     public:
         T() {}
         T(int, char) {}
     };
     // T a(); // (△) 비권장. T를 리턴하는 함수 a의 선언입니다.
     T a_11{}; // (O) T의 기본 생성자로 생성합니다.
    
     T b_11{10, 'a'}; // T(int, char)로 생성합니다.
     T{10, 'a'}; // T(int, char) 로 임시 개체를 생성합니다.
     T* c_11 = new T{10, 'a'}; // new 시 T(int, char)로 생성한 포인터를 d에 저장합니다.
     delete c_11;
    
  2. 클래스 멤버 변수 선언시 초기화에 사용할 수 있습니다.(멤버 선언부 초기화 참고)

    1
    2
    3
    4
    5
    
     class T {
     public:
         int m_A_11{0}; // 멤버 변수를 초기화합니다.
         char m_B_11{'a'}; 
     };     
    
  3. 클래스 생성자 초기화 리스트에서 사용할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     class T {
     public:
         int m_A_11; 
         char m_B_11;
     public:
         T(int a, char b) :
             m_A_11{a}, // 생성자 초기화 리스트에서 사용합니다.
             m_B_11{b} {}
     };
    

중괄호 복사 초기화 T t = {};, t = {};, f({}), return {}

T t = T{};중괄호 직접 초기화T{}임시 개체를 생성하고, 복사 생성자를 이용하여 t복사 생성하는 표현입니다. 만약 형변환 생성자explicit가 아니라면, 암시적 형변환이 지원되어 T t = {};와 같이 축약하여 표현할 수 있습니다.

암시적 형변환이므로, explicit로 정의하면 T t = T{};는 되지만, T t = {}; 는 컴파일 오류가 발생합니다.

또한 기존에는 생성자인자가 1개만 있을때에만 암시적 형변환을 차단하기 위해 explicit를 사용했었는데요, 이제는 중괄호 복사 초기화= {} 표현을 차단하기 위해 생성자인자가 여러개 있더라도 explicit를 사용할 수 있습니다.(생성자인자가 1개만 있을때에는 암시적 형변환이 되므로 explicit가 필수이지만, 여러개인 것은 중괄호 복사 초기화= {} 표현을 차단이 필요한 경우만 선택적으로 사용하시면 됩니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class T {
public:
    T(int) {}
    T(int, int) {}
};

// 암시적으로 T(int), T(int, int) 생성자를 호출하고, a_11과 b_11, c_11에 복사 생성합니다.
T a_11 = {1}; 
T b_11 = {1, 2};
T c_11 = T{1, 2};

class U {
public:
    explicit U(int) {}
    explicit U(int, int) {} // 생성자의 인자가 여러개이더라도 = {} 표현을 차단하기 위해 explicit를 사용합니다.
};

// (X) 컴파일 오류. explicit 생성자여서 암시적으로 생성자를 호출할 수 없습니다.
// U d_11 = {1};         
// U e_11 = {1, 2};
U f_11 = U{1, 2}; // 명시적으로 생성할 있습니다. 중괄호 직접 초기화인 U{1, 2}을 f_11에 복사 생성하는 표현입니다.

explicit를 뺏다고 할지라도, 복사 생성자가 없다면, T a_11{}은 되나 T b_11 = {}는 안됩니다. =하나 차이로요.

1
2
3
4
5
6
7
8
9
class T {
public:
    T() {}
    T(int) {} 
    T(const T&) = delete; // 복사 생성자를 사용할 수 없습니다.
};       

T a_11{10};
// T b_11 = {10}; // (X) 컴파일 오류. 복사 생성자를 사용할 수 없습니다.

중괄호 복사 초기화는 다음의 경우에 활용할 수 있습니다.

  1. 생성자 호출시 사용할 수 있습니다.

    이때, T t = {};은 컴파일러 최적화에 의해 T t{};와 동일하게 작동할 수 있습니다.(생성자 호출 및 함수 인수 전달 최적화 참고)

    다음 코드의 실행 결과를 보면,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     class T {
     public:
         T() {std::cout << "T : Default Constructor" << std::endl;}
         T(const T&) {std::cout << "T : Copy Constructor" << std::endl;}
         T& operator =(const T&) {std::cout << "T : operator =()" << std::endl;return *this;}
     };
    
     T a_11{}; // 기본 생성자 호출
     T b_11 = T{}; // T 기본 생성자를 호출하고, 복사 생성자를 호출. 컴파일러 최적화에 의해 2개의 생성을 1개의 생성으로 바꿈 
     T c_11 = {}; // T c_11 = T{}; 와 동일
    

    모두 기본 생성자를 1회만 호출하는 것을 알 수 있습니다.

    1
    2
    3
    
     T : Default Constructor // T a_11{};
     T : Default Constructor // T b_11 = T{};
     T : Default Constructor // T c_11 = {};
    

    (C++17~) 임시 구체화와 복사 생략 보증을 통해 컴파일러 의존적이었던 생성자 호출 및 함수 인수 전달 최적화, 리턴값 최적화등이 표준화 되었습니다.

  2. 대입문에 사용할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    
     class T {
     public:
         T() {std::cout << "T : Default Constructor" << std::endl;}
         T(const T&) {std::cout << "T : Copy Constructor" << std::endl;}
         T& operator =(const T&) {std::cout << "T : operator =()" << std::endl;return *this;}
     };
     T t_11;
     t_11 = {}; // t = T{};과 동일. 임시 개체가 생성되어 대입됩니다.
    

    다음 실행 결과를 보면, t_11 = {};임시 개체가 생성되는 것을 알 수 있습니다.

    1
    2
    3
    
     T : Default Constructor // T t_11;
     T : Default Constructor // t_11 = {}; 에서 {}는 사실 T{} 이므로 기본 생성자 호출
     T : operator =() // t_11 = {}; 에서 = 호출
    
  3. 함수 인수 전달에 사용할 수 있습니다.

    1
    2
    3
    
     void f(T param) {}
    
     f({}); // f(T{}); 와 동일. 임시 개체가 생성되어 전달됩니다.
    
  4. 리턴문에 사용할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     class T {
         int m_A;
         char m_B;
     public:
         T() {}
         T(int a, char b) : m_A(a), m_B(b) {}    
     };
    
     T Func_11() {
         return {10, 'b'}; // return T{10, 'b'}; 와 동일. 임시 개체가 생성되어 전달됩니다.
     }
    

중괄호 집합 초기화

집합 타입의 경우에는 기존에도 = {} 표현을 지원했는데요(배열 초기화구조체 초기화 참고),

C++11 부터는 {} 도 지원합니다.

중괄호 집합 초기화중괄호 직접 초기화와는 달리 생성자가 없어도 초기화할 수 있으며, 요소 갯수를 유추나 자동 제로 초기화의 특수한 기능도 수행합니다.

  1. 모든 멤버 변수public이고, 생성자가 없는 집합 타입

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
     class T {
     private:
         int m_X;
         int m_Y;
     public:
         T(int x, int y) : m_X(x), m_Y(y) {}
     };
    
     T a_11{1, 2}; // 중괄호 직접 초기화. 생성자를 호출합니다.
     T b_11 = {1, 2}; // 중괄호 복사 초기화. T(int x, int y)를 이용해서 생성하고 복사 생성자를 호출합니다.
    
     class U { // 집합 타입은 사용자 정의 생성자와 소멸자가 없어야 합니다.
     public: // 집합 타입은 모든 멤버 변수가 public 이어야 합니다
         int m_X;
         int m_Y;
     };
    
     U c_11{1, 2}; // 중괄호 집합 초기화. 생성자가 없더라도 멤버 변수들을 직접 초기화 합니다. 
     U d_11 = {1, 2}; // U d_11{1, 2}; 과 동일
    
  2. 요소 갯수 유추

    초기화 갯수 만큼 배열 크기를 유추합니다. 단, new[]로 생성하는 경우에는 유추하지 못하므로 배열 크기를 명시해야 합니다.

    1
    2
    3
    4
    
     int arr[] = {0, 1, 2}; // 초기화 갯수 만큼 배열 할당
     int arr_11[]{0, 1, 2}; // 초기화 갯수 만큼 배열 할당 
     int* ptr_11 = new int[3]{1, 2, 3}; // new[]로 생성하는 경우에는 배열 크기를 명시해야 합니다.
     delete ptr_11;  
    

    (C++20~) new[]에서 중괄호 집합 초기화로 배열 크기 추론이 추가되어 배열 크기를 명시하지 않아도 됩니다.

  3. 자동 제로 초기화

    1
    2
    3
    
     int arr[3] = {0, 1,}; // 초기값이 모자르면 0으로 채움
     int arr_11[3]{0, 1,}; // 초기값이 모자르면 0으로 채움
     EXPECT_TRUE(arr[2] == 0 && arr_11[2] == 0);
    

(C++20~) 지명 초기화가 추가되어 중괄호 집합 초기화시 변수명을 지명하여 값을 초기화 할 수 있습니다.

인자의 암시적 형변환 차단

중괄호 초기화생성자 인자암시적 형변환을 기존보다는 좀더 차단해 줍니다. 코딩 계약이 좀더 단단해 졌지만, 사용자 정의 형변환은 여전히 막지 못합니다.

  1. 실수에서 정수로 변환을 차단합니다.

    T a(3.14);double형이 int암시적으로 형변환되어 호출됩니다만(오버로딩된 함수 결정 규칙 참고), T b{3.14}; 은 컴파일 오류를 발생시킵니다.

    1
    2
    3
    4
    5
    6
    7
    
     class T {
     public:
         explicit T(int) {}
     };
    
     T a(3.14); // (△) 비권장. 3.14는 double입니다. int로 암시적으로 변환되어 초기화 됩니다.
     T b_11{3.14}; // (X) 컴파일 오류. 암시적 변환을 차단합니다.
    

    암시적 형변환중 더 큰 데이터를 저장할 수 있는 타입으로 변환하는 승격 변환(boolint로, charint로, intdouble로 변환 등)은 차단하지 못합니다.

    1
    2
    3
    4
    5
    6
    7
    
     class T {
     public:
     explicit T(double) {}
     };
    
     T a(3); // (△) 비권장. int가 double로 승격 변환되어 초기화 됩니다.
     T b_11{3}; // (△) 비권장. int가 double로 승격 변환되어 초기화 됩니다.
    
  2. double에서 float으로의 변환을 경고합니다. 단, 상수 표현식에서 해당 값을 저장할 수 없다면 컴파일 오류이고, 해당 값을 저장할 수 있다면 허용합니다.(int에서 char 변환도 동일합니다.)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     class T {
     public:
         explicit T(float) {};
     };  
    
     double doubleVal = 3.14;
     // T a_11{doubleVal}; // (X) 컴파일 경고. double을 float으로 변환하는건 경고합니다.
        
     T b_11{3.14}; // 상수 표현식은 값 범위라면 허용. 3.14는 float이 저장할 수 있어서 허용
    
  3. int에서 char 변환도 double에서 float의 변환과 동일합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     class T {
     public:
         explicit T(char) {};
     }; 
    
     int intVal = 10;
     // T a_11{intVal}; // (X) 컴파일 경고. int를 char로 변환하는건 경고합니다.
    
     T b_11{10}; // 상수 표현식은 값 범위라면 허용. 10은 char에서 저장할 수 있어서 허옹
     // T c_11{255}; // (X) 컴파일 오류. 255는 char에서 저장할 수 없음.
    
  4. 포인터 타입에서 bool로의 변환을 경고해 줍니다.

    1
    2
    3
    
     int* ptr;
     bool b(ptr);
     bool b_11{ptr}; // (X) 컴파일 경고. int*에서 bool로 변환하는건 경고합니다.
    
  5. 사용자가 형변환 생성자를 작성하면 암시적 형변환이 허용됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     class A {};
     class B {
     public:
         B(A) {} // explicit가 없는 형변환 생성자. A로 암시적으로 생성됩니다.
     };  
     class T {
     public:
         explicit T(B) {}
     };     
    
     A a;
     T t_11{a}; // (△) 비권장. A->B로의 암시적 변환을 허용하면 차단되지 않습니다.   
    

중괄호 초기화 중첩

중괄호 초기화를 중첩해서 사용할 수 있는데요, 기본 타입은 지원하지 않습니다.

기본 타입과 개체 타입

값 초기화의 일반적인 형태는 다음과 같습니다.

1
2
int a = 10;
int b(10);

그리고, 다음처럼 복사 생성자를 이용하여 대입하는 표현이 있습니다.

1
2
int a(int(10)); // int(10)으로 생성한 개체를 int a 의 복사 생성자를 호출하여 생성합니다.
int b = int(10); // int(10)으로 생성한 개체를 int a 의 복사 생성자를 호출하여 생성합니다.

요걸 중괄호 초기화로 수정하면 다음과 같습니다.

1
2
3
4
int a_11{
    int{10}
};
int b_11 = int{10};

중괄호 복사 초기화의 축약 표현을 사용해서 int{10}{10}으로 바꿔보면 기본 타입중괄호 초기화 중첩을 지원하지 않는다며 컴파일 오류가 납니다.

1
2
3
4
// int c_11{ // (X) 컴파일 오류. 기본 타입은 중괄호 중첩을 지원하지 않습니다. 
//    {10}
// };
int d_11 = {10};

하지만, 개체 타입이면 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T {
public:
    T(int) {}    
};

T a_11{
    T{10} // (O)
}; 
T b_11 = T{10}; // (O)

// 축약형
T a_11{ 
    {10} // (O) T{10}
}; 
T b_11 = {10}; // (O)

배열

배열을 초기화 할때 다음처럼 중괄호로 초기화 할 수 있는데요,

1
2
3
4
5
6
7
8
9
10
11
class A {
    int m_X;
    int m_Y;
public:
    A(int x, int y) : m_X{x}, m_Y{y} {}
};

A arr_11[]{
    A{1, 2}, // 배열의 첫번째 요소
    A{2, 3} // 배열의 두번째 요소
};

중괄호 복사 초기화의 축약 표현을 사용할 수 있습니다.

1
2
3
4
A arr_11[]{
    {1, 2}, // A{1, 2} 처럼 동작합니다.
    {2, 3} // A{2, 3} 처럼 동작합니다.
};

내부 개체

또한 내부 멤버 변수 개체를 초기화 할때에도 축약 표현을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
    int m_X;
    int m_Y;
public:
    A(int x, int y) : m_X{x}, m_Y{y} {}
};
class B {
    int m_Val;
    A m_A;
public:
    B(int val, A a) : m_Val{val}, m_A{a} {}
};

B b_11{1, {2, 3}}; // B b_11{1, A{2, 3}}; 처럼 동작합니다.

집합 타입

멤버 변수public집합 타입이라면, 값 생성자가 없어도 배열이나 구조체처럼 중괄호 집합 초기화로 초기화할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
class A {
public:
    int m_X; // 내부 멤버가 public인 집합 타입이면 값 생성자가 없어도 됩니다.
    int m_Y;
};
class B {
public:
    int m_Val; // 내부 멤버가 public인 집합 타입이면 값 생성자가 없어도 됩니다.
    A m_A;
};

B b_11{1, {2, 3}}; // B b_11 = B{1, A{2, 3}}; 처럼 동작합니다.

initializer_list

C++11의 컨테이너중괄호 초기화를 이용하여 쉽게 초기값을 입력할 수 있도록 initializer_list를 사용한 생성자를 제공합니다.

vector의 경우 초기 요소들을 입력할때 push_back()을 일일히 사용해야 되서 코드 작성이 번거로웠는데(vector 의 삽입과 삭제 참고), initializer_list 생성자를 이용하면 {}로 한번에 초기 요소들을 입력할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 이전 방식
std::vector<int> v;
v.push_back(1); // 일일이 push_back을 작성해야 해서 번거롭습니다.
v.push_back(2);
EXPECT_TRUE(v[0] == 1 && v[1] == 2);

// {} 를 이용하여 initializer_list로 초기화
std::vector<int> v1_11{1, 2};
EXPECT_TRUE(v1_11[0] == 1 && v1_11[1] == 2);       

// = {} 형태도 지원
std::vector<int> v2_11 = {1, 2};
EXPECT_TRUE(v2_11[0] == 1 && v2_11[1] == 2); 

만약 explicit를 사용하지 않은 개체라면, 암시적 형변환되므로 다음처럼 축약하여 초기화 할 수 있습니다. 이때, 값 생성자A_11을 생성하고, 복사 생성자로 생성한 복제본을 vector에서 관리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A_11 {
private:
    int m_Val;
public:
    A_11(int val) : m_Val{val} { // explicit가 없습니다. int가 암시적으로 A_11로 형변환됩니다.
        std::cout << "A : Value Constructor" << std::endl;    
    }
    A_11(const A_11& other) : m_Val(other.m_Val) {
        std::cout << "A : Copy Constructor" << std::endl;
    }

    int GetVal() const {return m_Val;}
};

std::vector<A_11> v1{ // Value Constructor 3회, Copy Constructor 3회 호출
    1, 2, 3 // (O) 축약 표현. 1, 2, 3은 A_11로 암시적으로 형변환 됩니다.
}; 
std::vector<A_11> v2{ // Value Constructor 3회, Copy Constructor 3회 호출
    {1}, {2}, {3} // (O) 축약 표현. 암시적 형변환이 되므로 A_11{1}, A_11{2}, A_11{3}의 축약 표현이 가능합니다.
}; 
std::vector<A_11> v3{ // Value Constructor 3회, Copy Constructor 3회 호출
    A_11{1}, A_11{2}, A_11{3}
}; 

만약 explicit를 사용한다면, 축약 표현은 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A_11 {
private:
    int m_Val;
public:
    explicit A_11(int val) : m_Val{val} { // explicit입니다. 암시적 형변환이 되지 않습니다.
        std::cout << "A : Value Constructor" << std::endl;    
    }
    A_11(const A_11& other) : m_Val(other.m_Val) {
        std::cout << "A : Copy Constructor" << std::endl;
    }

    int GetVal() const {return m_Val;}
};

// std::vector<A_11> v1{ 
//     1, 2, 3 // (X) 컴파일 오류. int는 A_11로 암시적으로 형변환 되지 않습니다. 
// }; 
// std::vector<A_11> v2{ 
//     {1}, {2}, {3} // (X) 컴파일 오류. 암시적 형변환이 되지 않으므로, 축약 표현은 사용할 수 없습니다.
// }; 
std::vector<A_11> v3{ // Value Constructor 3회, Copy Constructor 3회 호출
    A_11{1}, A_11{2}, A_11{3}
}; 

initializer_list 멤버 함수

initializer_list는 요소를 탐색할 수 있는 기본적인 멤버 함수만 제공합니다.

항목 내용
size() (C++11~) 요소 갯수를 리턴합니다.
begin() (C++11~) 첫번째 요소의 const 포인터를 리턴합니다.
end() (C++11~) 마지막 요소의 다음 위치의 const 포인터를 리턴합니다.

initializer_list의 암시적 생성

다음의 경우 중괄호 초기화 표현식은 initializer_list를 암시적으로 자동 생성합니다.

  1. 생성자나 함수의 인자initializer_list 인 경우

    1
    2
    3
    4
    5
    6
    7
    
     class T_11 {
     public:
         explicit T_11(std::initializer_list<int>) {}
     };
     T_11 t{
         {1, 2, 3} // {1, 2, 3} 은 initializer_list를 생성해서 전달합니다.
     }; 
    
  2. 복사 대입 연산자인자initializer_list 인 경우

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
     class T_11 {
     public:
         void operator =(std::initializer_list<int> data_11) { // 복사 대입 연산자가 initializer_list를 전달받습니다.
             std::vector<int> v;
             const int* itr = data_11.begin();
             const int* endItr = data_11.end();
             for (;itr != endItr; ++itr) {
                 v.push_back(*itr);
             }
         }    
     };
     T_11 t;
     t = {1, 2, 3}; // {1, 2, 3} 은 initializer_list를 생성해서 전달합니다.  
    
  3. 범위 기반 for()에서 범위 표현식으로 사용하는 경우

    1
    2
    3
    4
    
     std::vector<int> v;
     for (int a_11 : {1, 2, 3}) { // {1, 2, 3} 은 initializer_list를 생성하고 범위 기반 for문에서 사용됩니다.
         v.push_back(a_11);
     }  
    

중괄호 초기화 우선 순위

중괄호 초기화는 기존 중괄호 집합 초기화와의 호환성과 initializer_list 의 편의성을 위해 다음의 우선 순위에 맞춰 동작합니다.

  1. 문자 배열에서 문자열 상수로 초기화 합니다.

    1
    
     char arr_11[] = {"abc"}; // char arr_11[] = "abc"; 와 동일
    
  2. 집합 타입인 경우 중괄호 집합 초기화를 합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     class T { // 집합 타입은 사용자 정의 생성자와 소멸자가 없어야 합니다.
     public: // 집합 타입은 모든 멤버 변수가 public 이어야 합니다.
         int m_X;
         int m_Y;
     };
        
     T a{1, 2};
     T b = {10, 20};
    
     EXPECT_TRUE(a.m_X == 1 && a.m_Y == 2);
     EXPECT_TRUE(b.m_X == 10 && b.m_Y == 20);   
    

    만약 멤버 변수private라면 집합 타입이 아니므로 생성자를 호출합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    
     class T {
     private: // 집합 타입이 아닙니다. 집합 타입은 모든 멤버 변수가 public 이어야 합니다.
         int m_X;
         int m_Y;
     };
        
     T a{1, 2}; // (X) 컴파일 오류. 집합 타입이 아니어서 생성자를 호출하는데, 생성자가 없습니다.
     T b = {10, 20}; // (X) 컴파일 오류. 집합 타입이 아니어서 생성자를 호출하는데, 생성자가 없습니다.
    
  3. 중괄호가 비어 있고 기본 생성자가 있으면, 기본 생성자를 호출합니다.

    1
    2
    
     // std::vector<int> v(); // (△) 비권장. std::vector<int>를 리턴하는 함수 v의 선언입니다.
     std::vector<int> v_11{}; // 기본 생성자
    
  4. initializer_list를 사용한 생성자가 있으면 해당 생성자를 호출합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     class T_11 {
     public:
         explicit T_11(int, int, int, int, int) {}
         explicit T_11(std::initializer_list<int>) {}
         explicit T_11(std::initializer_list<int>, int, int) {}
     };
     T_11 a{1, 2, 3, 4, 5}; // T_11(std::initializer_list<int>)
     T_11 b{ 
         {1, 2, 3}, 4, 5 // T_11(std::initializer_list<int>, int, int)
     }; 
    

기존 생성자와 initializer_list 생성자와의 충돌

다음 코드는 중괄호 복사 초기화의 축약형처럼 보이지만 사실은 initializer_list를 암시적으로 생성합니다.(initializer_list의 암시적 생성 참고)

1
2
3
4
5
6
7
8
9
class T_11 {
public:
    T_11() {std::cout << "T_11::Default Constructor" << std::endl;}
    T_11(const T_11&) {std::cout << "T_11::Copy Constructor" << std::endl;}
    T_11(std::initializer_list<int>) {std::cout << "T_11::initializer_list Constructor" << std::endl;}    
};
T_11 t{
    {} // initializer_list를 암시적으로 생성합니다.
};

따라서, T_11{}를 생성하고 t복사 생성자를 호출할 것 같지만, initializer_list 생성자를 호출합니다.

1
T::initializer_list Constructor

만약 명시적으로 T_11{}와 같이 사용한다면,

1
2
3
4
5
6
7
8
9
class T_11 {
public:
    T_11() {std::cout << "T_11::Default Constructor" << std::endl;}
    T_11(const T_11&) {std::cout << "T_11::Copy Constructor" << std::endl;}
    T_11(std::initializer_list<int>) {std::cout << "T_11::initializer_list Constructor" << std::endl;}    
};
T_11 t{
    T_11{} // 명시적으로 사용했습니다.
};

기본 생성자를 호출합니다. 약간의 코딩 차이로 다른 결과가 나오게 되니 주의해야 합니다.

1
T::Default Constructor

또한 중괄호 초기화 우선 순위 4번에 따라 기존 생성자를 가리는 심각한 문제가 발생합니다.

vector에서의 경우를 살펴보죠.

요소의 초기 갯수를 지정하는 vector(size_t count);를 호출하기 위해 vector<int> v_11{2};와 같이 호출했다고 합시다.

하지만, vectorinitializer_list 생성자가 있기 때문에, 우선순위 4에 의해 initializer_list 생성자가 호출됩니다.

즉, 요소가 2개인 vector를 생성하는게 아니라, 2값인 요소 하나만 있는 vector를 생성합니다. 그래서 요소 갯수가 2개인 vector를 생성하려면 v3(2)와 같이 괄호(())를 사용해야 합니다.

1
2
3
4
5
std::vector<int> v1(2); // 요소 갯수가 2개인 vector 생성
EXPECT_TRUE(v1.size() == 2 && v1[0] == 0 && v1[1] == 0);

std::vector<int> v2_11{2}; // 요소값이 2인 vector 생성
EXPECT_TRUE(v2_11.size() == 1 && v2_11[0] == 2);  

이렇게 initializer_list 생성자의 우선 순위가 높은점은 유지보수시 심각한 오류를 유발할 수 있습니다.

initializer_list 생성자가 없는 개체를 중괄호 초기화를 사용하여 열심히 코딩해 뒀는데, 누군가가 initializer_list 생성자를 만들었다면, 기존 생성자 호출이 의도치 않게 initializer_list 생성자로 바껴 버릴 수 있거든요.

결국 우리의 선택지는 다음 세개중 하나입니다. 사람마다 선호하는게 다를 수 있을텐데요, 저는 2번이 제일 나은것 같습니다. 1번은 구더기 무서워서 장을 못담그는 셈이고, 3번은 지키기 어려운 규칙이거든요. 지키기 어려운 규칙은 결국 언젠가는 무너집니다.

  1. 중괄호 초기화를 사용하지 않습니다.
  2. initializer_list 생성자가 필요한 개체를 사전에 미리 예측하고, 미리 만들어 둡니다. 그리고, 유지 보수시에 절대 initializer_list 생성자를 만들지 않습니다.
  3. 유지 보수시 initializer_list 생성자를 만들면, 기존 코드를 정밀 검토하여 모두 수정합니다.

멤버 선언부 초기화

기존에는 초기화 리스트에서만 비정적 멤버 변수를 초기화 할 수 있었는데요(초기화 리스트 참고),

C++11 부터는 비정적 멤버 변수 선언시에도 초기화 할 수 있습니다. 단, 클래스 정적 멤버 변수는 기존과 동일하게 별도 정의부에서 초기화해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
    explicit A(int, int) {}    
};

class T {
    int m_A_11 = 0; // C++11. 멤버 변수 초기화
    int m_B_11{0}; // C++11. 중괄호 초기화
    int m_C_11 = {0}; // C++11. 중괄호 초기화
    int* m_D__1 = nullptr; // C++11. 포인터 초기화
    std::vector<int> m_E_11{1, 2, 3}; // C++11. 중괄호 초기화
    A m_F_11{10, 20}; // C++11. 중괄호 초기화
    // A m_F_11(10, 20); // (X) 컴파일 오류. A를 리턴하는 m_F 함수 정의

    const int m_G_11 = 0; // 기존과 동일하게 상수 멤버 변수 초기화
    static const int m_H_11 = 0; // 기존과 동일하게 선언부에 정적 상수 멤버 변수 초기화
    // static int m_H_11 = 0; // (X) 컴파일 오류. 기존과 동일하게 정적 멤버 변수는 별도 초기화해야 함 
};  

(C++17~) 인라인 변수가 추가되어 헤더 파일에 정의된 변수를 여러개의 cpp에서 #include 하더라도 중복 정의 없이 사용할 수 있습니다. 또한, 클래스 정적 멤버 변수를 선언부에서 초기화 할 수 있습니다.

(C++14~) 비정적 멤버 변수의 멤버 선언부 초기화시 집합 초기화

C++11의 중괄호 집합 초기화를 이용하면, 다음과 같이 집합 타입에 대해 초기화를 할 수 있습니다.

1
2
3
4
5
6
7
8
class A { // 집합 타입은 사용자 정의 생성자와 소멸자가 없고, 모든 멤버 변수가 public 이어야 합니다.
public: 
    int m_X;
    int m_Y;
};

A a_11{0, 1}; // 중괄호 집합 초기화
EXPECT_TRUE(a_11.m_X == 0 && a_11.m_Y == 1);

하지만, C++14 이전 버전은 멤버 선언부에 초기화와 함께 사용하면 컴파일 오류가 발생했는데요, C++14 부터는 이를 완화하여 비정적 멤버 변수멤버 선언부에 초기화하더라도 중괄호 집합 초기화를 사용할 수 있습니다.

다음 코드는 C++14 이전에는 컴파일 오류였지만, C++14 부터는 컴파일 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
class A_11 { // 집합 타입은 사용자 정의 생성자와 소멸자가 없고, 모든 멤버 변수가 public 이어야 합니다.
public: 
    int m_X{0}; // 비정적 멤버 선언부 초기화
    int m_Y{1};
};

// (X) ~C++14 컴파일 오류. no matching function for call to 'A_11::A_11(<brace-enclosed initializer list>)'
// (O) C++14~
A_11 a_14{0, 1}; // A_11 에는 생성자가 없습니다.
                 // 따라서 생성자를 호출하는 중괄호 직접 초기화가 아니라 
                 // 중괄호 집합 초기화 입니다.
EXPECT_TRUE(a_14.m_X == 0 && a_14.m_Y == 1);   

(C++20~) 지명 초기화

기존의 중괄호 집합 초기화는 중괄호 내에서 선언 순서대로 나열해서 초기화 했습니다. 만약 멤버 변수가 많다면 어느것을 초기화 하는지 헷갈릴 수가 있는데요,

1
2
3
4
5
6
class T_11 {
public:
    int a, b, c, d, e;
};

T_11 obj_11{1, 2, 3, 4, 5}; // 갯수가 많아지면 어느값이 어느 멤버 변수를 초기화 하는지 헷갈립니다.

C++20 부터는 지명 초기화가 추가되어 중괄호 집합 초기화시 변수명을 지명하여 값을 초기화 할 수 있습니다.

1
T_11 obj_20{.a = 1, .b = 2, .c = 3, .d = 4, .e = 5}; // 초기화할 변수명과 값을 지명합니다.

다음과 같은 특징이 있습니다.

  1. 집합 타입만 지원합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
     class T { // 집합 타입은 사용자 정의 생성자와 소멸자가 없어야 합니다.
     public: // 집합 타입은 모든 멤버 변수가 public 이어야 합니다.
         int m_Val;
     };  
     class U {
     private: // 집합 타입이 아닙니다. 집합 타입은 모든 멤버 변수가 public 이어야 합니다.
         int m_Val;
     };      
     class V {
     public:
         int m_Val;
         V() {}; // 사용자 정의 생성자가 있어서 집합 타입이 아닙니다.
     };
    
     T t_20{.m_Val = 1};
     U u_20{.m_Val = 1}; // (X) 컴파일 오류. 집합 타입이 아닙니다.
     V v_20{.m_Val = 1}; // (X) 컴파일 오류. 집합 타입이 아닙니다.  
    
  2. 배열은 지원하지 않습니다.

    1
    2
    
     // 배열은 지원하지 않습니다.
     int arr_20[3]{[1] = 1}; // (X) 컴파일 오류. 배열은 지원하지 않습니다.
    
  3. 멤버 변수 선언과 초기화 순서가 동일해야 합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    
     class T {
     public:
         int m_X;
         int m_Y;
         int m_Z;
     };
     T a_20{.m_X = 0, .m_Y = 1, .m_Z = 2}; // (O)
     // T b_20{.m_X = 0, .m_Z = 1, .m_Y = 2}; // (X) 컴파일 오류. m_Z와 m_Y가 선언 순서와 다릅니다.
    
  4. 지명 초기화와 비지명 초기화를 혼합해서 사용할 수 없습니다.

    1
    2
    3
    4
    5
    6
    7
    
      class T {
     public:
         int m_X;
         int m_Y;
     };  
    
     T a_20{.m_X = 0, 1}; // (X) 컴파일 오류. m_X를 지명했으면, 다른 멤버도 지명해야 합니다.
    
  5. 특정 항목을 생략할 수 있습니다. 생략된 것은 기본 생성자로 초기화됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     class T {
     public:
         int m_X;
         int m_Y;
         int m_Z;
     };
    
     T t_20{.m_X = 0, .m_Z = 2}; // (O)
     EXPECT_TRUE(t_20.m_Y == 0); // 생략된 것은 기본 생성자로 초기화됩니다. 
    
  6. 내부 개체를 지명할 수 없습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
     class Inner { // 집합 타입입니다.
     public:
         int m_Val1; 
         int m_Val2;
     };
        
     class T {
     public:
         int m_Val{0};
         Inner m_Inner; // 내부 개체 입니다.
     };        
    
     T a_20{0, {1, 2}}; // {1, 2}는 Inner 개체를 초기화 합니다.
     T b_20{.m_Val = 0, .m_Inner = {1, 2}}; // (O)
     T c_20{.m_Val = 0, .m_Inner.m_Val1 = 1, .m_Inner.m_Val2 = 2}; // (X) 컴파일 오류. 지명 초기화는 내부 개체를 지명할 수 없습니다.
    
  7. 인자의 암시적 형변환 차단과 마찬가지로 인자를 평가합니다.

    1. 실수에서 정수로 변환을 차단하며,
    2. double에서 float 변환은 경고 합니다. 단, 상수 표현식에서 해당 값을 저장할 수 없으면 오류이고, 해당 값을 저장할 수 있다면 허용합니다.
    3. int에서 char 변환도 double에서 float 변환과 동일합니다.
    4. 포인터 타입에서 bool로의 변환을 경고해 줍니다.
    5. 사용자가 형변환 생성자를 작성하면 암시적 형변환이 허용됩니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
     class A {};
     class B {
     public:
         B(A) {} // explicit가 없는 형변환 생성자. A로 암시적으로 생성됩니다.
     };
     class T {
            
     public:
         int m_X{1};
         int m_Y{2};
         float m_Z{3.F}; // float 입니다.
         bool m_Bool;
         B m_B{A{}}; // A 기본 생성자로 생성합니다.
     };   
    
     // T a_20{.m_X = 1.5}; // (X) 컴파일 오류. 실수를 정수로 변환시 컴파일 오류가 발생합니다.
    
     // double doubleVal{3.14};
     // T b_20{.m_Z = doubleVal}; // (X) 컴파일 경고. double을 float으로 변환하는건 경고합니다.
     T c_20{.m_Z = 3.14}; // 상수값을 저장할 수 있다면 허용합니다.
    
     int* ptr;
     // T d_20{.m_Bool = ptr}; // (X) 컴파일 경고. 포인터 타입에서 bool 변환하는건 경고합니다.
    
     T k_20{.m_B = A{}}; // (△) 비권장. A->B로의 암시적 변환을 허용하면 차단되지 않습니다.
    

한편, C언어에서는

  • 배열을 지원하고,
  • 선언 순서와 다르게 지정할 수도 있고,
  • 지명 초기화와 비지명 초기화를 혼합해서 사용할 수 있으며
  • 내부 개체를 지명할 수 있습니다.

훨씬 자유도가 높은데요, C언어와 C++언어가 다른 이유는,

C++에서는

  • 배열람다 표현식과 충돌하고,
  • 멤버가 생성의 역순으로 소멸되어야 하고 초기치는 정확한 순서를 유지해야 하기 때문에, 선언 순서 불일치나 혼합이나 내부 개체 지명을 허용하지 않는다고 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
struct A { 
    int m_X;
    int m_Y; 
};
struct B { 
    A m_A; 
};

int arr_20[3] = {[1] = 5}; // valid C, invalid C++ (배열)   
A a_20 = {.m_Y = 1, .m_X = 2}; // valid C, invalid C++ (순서 불일치)
A b_20 = {.m_X = 1, 2}; // valid C, invalid C++ (혼합)
B c_20 = {.m_A.m_X = 0}; // valid C, invalid C++ (내부 개체 지명)

(C++20~) 비트 필드 선언부 초기화

C++20 부터는 비트 필드를 선언부에서 초기화할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
class Flag_20 {
public:
    unsigned char m_Val1 : 2{3}; // 2bit 이므로 값의 범위는 00(0), 01(1), 10(2), 11(3)
    unsigned char m_Val2 : 3{7}; // 3bit 이므로 값의 범위는 000(0), 001(1), 010(2), 011(3), 100(4), 101(5), 110(6), 111(7)
};

Flag_20 flag;
EXPECT_TRUE(sizeof(flag) == sizeof(unsigned char));

EXPECT_TRUE(flag.m_Val1 == 3); // 3 저장
EXPECT_TRUE(flag.m_Val2 == 7); // 7 저장

(C++20~) new[]에서 중괄호 집합 초기화로 배열 크기 추론

기존에 중괄호 집합 초기화는 배열 크기를 추론했습니다.

1
int arr[]{1, 2, 3}; // 중괄호 집합 초기화로 배열 크기를 추론합니다.

하지만 new[]에서는 배열 크기를 추론하지 않고 컴파일 오류를 발생시켰는데요(중괄호 집합 초기화 참고),

1
2
//int* arr_11 = new int[]{1, 2, 3}; // (X) 컴파일 오류. new[]에서 중괄호 집합 초기화로 배열 크기를 추론하지 못합니다.
int* arr_11 = new int[3]{1, 2, 3}; // 배열 크기를 명시해야 합니다.

C++20 부터는 new[]에서 중괄호 집합 초기화로 배열 크기 추론이 추가되어 배열 크기를 명시하지 않아도 됩니다.

1
int* arr_20 = new int[]{1, 2, 3}; // new[]를 사용해도 중괄호 집합 초기화로 배열 크기를 추론합니다.

태그:

카테고리:

업데이트:

댓글남기기