-----------------
title : 씹어먹는 C++ - <16 - 1. C++ 유니폼 초기화(Uniform Initialization)>
cat_title : 16 - 1. C++ 유니폼 초기화(Uniform Initialization)
path : /C++
publish_date : 2019-06-23
next_page : 293
chapter : C++ 유니폼 초기화
-----------------

이번 강좌에서는

* 균일한 초기화 (Uniform initialization)
* 초기화자 리스트 (`initializer_list`)

에 대해 다룹니다.

![](/img/ChewingCpplogo.png)

안녕하세요 여러분! 이번 강좌에서는 C++ 11 에서 추가된 기능인 **균일한 초기화(Uniform Initialization)** 에 대해 살펴보도록 하겠습니다. 

아마도 여러분들은 아래와 같은 실수를 한 번쯤 하셨을 것이라 생각합니다.

```cpp-formatted
#include <iostream>

class A {
 public:
  A() { std::cout << "A 의 생성자 호출!" << std::endl; }
};

int main() {
  A a();  // ?
}
```

성공적으로 컴파일 하였다면

```exec

```

놀랍게도 아무것도 출력되지 않습니다. 왜일까요?

```cpp-formatted
A a();  // ?
```

왜냐하면 사실은 위 코드가 A 의 객체 a 를 만든것이 아니라, A 를 리턴하고, 인자를 받지 않는 함수 a 를 정의한 것이기 때문입니다. 왜냐하면 C++ 의 컴파일러는 **함수의 정의처럼 보이는 것들은 모두 함수의 정의로 해석** 하기 때문입니다.

심지어 아래와 같은 코드는 더 헷갈립니다.

```cpp-formatted
#include <iostream>

class A {
 public:
  A() { std::cout << "A 의 생성자 호출!" << std::endl; }
};

class B {
 public:
  B(A a) { std::cout << "B 의 생성자 호출!" << std::endl; }
};

int main() {
  B b(A());  // 뭐가 출력될까요?
}
```

성공적으로 컴파일 하였다면

```exec
```

와 같이 아무 것도 출력되지 않습니다.

사실 위 코드를 보면 마치 b 라는 클래스 B 의 객체를 생성하는 것 같이 보이지만, 사실은 **인자로 A 를 리턴하고 인자가 없는 함수를 받으며, 리턴 타입이 B 인 함수 b** 를 정의한 것입니다.

상당히 골치 아픈 일입니다. 이러한 문제가 발생하는 것은 () 가 함수의 인자들을 정의하는데도 사용되고, 그냥 일반적인 객체의 생성자를 호출하는데에도 사용되기 때문입니다.

따라서 C++ 11 에서는 이러한 문제를 해결하기 위해 **균일한 초기화(Uniform Initialization)** 라는 것을 도입하였습니다.

### 균일한 초기화 (Uniform Initialization)

균일한 초기화 문법을 사용하기 위해서는 생성자를 호출하기 위해 `()` 를 사용하는 대신에 `{}` 를 사용하면 끝입니다.

```cpp-formatted
#include <iostream>

class A {
 public:
  A() { std::cout << "A 의 생성자 호출!" << std::endl; }
};

int main() {
  A a{};  // 균일한 초기화!
}
```

성공적으로 컴파일 하였다면

```exec
A 의 생성자 호출!
```

위와 같이 제대로 생성자가 호출되었음을 알 수 있습니다.

중괄호를 이용해서 생성자를 호출하는 문법은 동일합니다. 그냥 기존에 `()` 자리를 `{}` 로 바꿔주기만 하면 됩니다. 하지만, `()` 를 이용한 생성과 `{}` 를 이용한 생성의 경우 한 가지 큰 차이가 있는데 바로 일부 암시적 타입 변환들을 불허하고 있다는 점입니다.

예를 들어서 아래 코드를 살펴봅시다.

```cpp-formatted
#include <iostream>

class A {
 public:
  A(int x) { std::cout << "A 의 생성자 호출!" << std::endl; }
};

int main() {
  A a(3.5);  // Narrow-conversion 가능
  A b{3.5};  // Narrow-conversion 불가
}
```

컴파일 하였다면 아래와 같은 오류가 발생합니다.

```compiler-warning
test.cc: In function ‘int main()’:
test.cc:10:10: error: narrowing conversion of ‘3.5e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
   A b{3.5}; // Narrow-conversion 불가
          ^
A 의 생성자 호출!
```

보시다시피

```cpp-formatted
A a(3.5);  // Narrow-conversion 가능
```

위 코드는 성공적으로 컴파일 되었고 `x` 에는 `3.5` 의 정수 캐스팅 버전이 3 이 전달됩니다. (한 번 아래 `b` 생성을 지워보고 실행해보세요.)

반면에

```cpp-formatted
A b{3.5};  // Narrow-conversion 불가
```

의 경우 위와 같이 `double` 인 3.5 를 `int` 로 변환할 수 없다는 오류가 발생하였습니다.

그 이유는 중괄호를 이용해서 생성자를 호출하는 경우 아래와 같은 암시적 타입 변환들이 불가능해집니다. 이들은 전부 **데이터 손실이 있는(Narrowing) 변환** 입니다.

* 부동 소수점 타입에서 정수 타입으로의 변환 (우리의 예시지요)
* `long double` 에서 `double` 혹은 `float` 으로의 변환, `double` 에서 `float` 으로의 변환.
* 정수 타입에서 부동 소수점 타입으로의 변환

등등이 있습니다. 자세한 예시들은 [여기](https://en.cppreference.com/w/cpp/language/list_initialization) 에서 확인할 수 있습니다.

따라서 `{}` 를 사용하게 된다면, 위와 같이 원하지 않는 타입 캐스팅을 방지해서 미연에 오류를 잡아낼 수 있습니다.

`{}` 를 이용한 생성의 또 다른 쓰임새로 함수 리턴 시에 굳이 생성하는 객체의 타입을 다시 명시 하지 않아도 됩니다.

```cpp-formatted
#include <iostream>

class A {
 public:
  A(int x, double y) { std::cout << "A 생성자 호출" << std::endl; }
};

A func() {
  return {1, 2.3};  // A(1, 2.3) 과 동일
}

int main() { func(); }
```

성공적으로 컴파일 하였다면

```exec
A 생성자 호출
```

와 같이 잘 나옵니다. `{}` 를 이용해서 생성하지 않았더라면 `A(1, 2.3)` 과 같이 클래스를 명시해줘야만 했지만, `{}` 를 이용할 경우 컴파일러가 알아서 함수의 리턴타입을 보고 추론해줍니다.

`{}` 의 쓰임새는 이 뿐만이 아닙니다. 아래를 보시지요.

### 초기화자 리스트 (Initializer list)

배열을 정의할 때 우리는 다음과 같이 작성하였습니다.

```cpp-formatted
int arr[] = {1, 2, 3, 4};
```

그렇다면 중괄호를 이용해서 마찬가지 효과를 낼 수 없을까요? 예를 들면

```cpp-formatted
vector<int> v = {1, 2, 3, 4};
```

와 같이 말이지요. 근데, 놀랍게도 C++ 11 에서 부터 이와 같은 문법을 사용할 수 있게 되었습니다.

```cpp-formatted
#include <iostream>

class A {
 public:
  A(std::initializer_list<int> l) {
    for (auto itr = l.begin(); itr != l.end(); ++itr) {
      std::cout << *itr << std::endl;
    }
  }
};

int main() { A a = {1, 2, 3, 4, 5}; }
```

성공적으로 컴파일 하였다면

```exec
1
2
3
4
5
```

와 같이 나옵니다.

`initializer_list` 는 우리가 `{}` 를 이용해서 생성자를 호출할 때, 클래스의 생성자들 중에 `initializer_list` 를 인자로 받는 생성자가 있다면 전달됩니다.

```lec-warning
`()` 를 사용해서 생성자를 호출한다면 `intializer_list` 가 생성되지 않습니다.
```

`initializer_list` 를 이용하면 컨테이너들을 간단하게 정의할 수 있습니다. 예를 들어서

```cpp-formatted
#include <iostream>
#include <map>
#include <string>
#include <vector>

template <typename T>
void print_vec(const std::vector<T>& vec) {
  std::cout << "[";
  for (const auto& e : vec) {
    std::cout << e << " ";
  }
  std::cout << "]" << std::endl;
}

template <typename K, typename V>
void print_map(const std::map<K, V>& m) {
  for (const auto& kv : m) {
    std::cout << kv.first << " : " << kv.second << std::endl;
  }
}

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};
  print_vec(v);

  std::cout << "----------------------" << std::endl;
  std::map<std::string, int> m = {
    {"abc", 1}, {"hi", 3}, {"hello", 5}, {"c++", 2}, {"java", 6}};
  print_map(m);
}
```

성공적으로 컴파일 하였다면

```exec
[1 2 3 4 5 ]
----------------------
abc : 1
c++ : 2
hello : 5
hi : 3
java : 6
```

와 같이 나옵니다.

```cpp-formatted
std::vector<int> v = {1, 2, 3, 4, 5};
```

`vector` 의 경우 생각했던대로, `vector` 의 원소들을 그냥 나열해주면 됩니다. 마치 이전에 배열을 정의할 때 처럼 말이지요.

```cpp-formatted
std::map<std::string, int> m = {
  {"abc", 1}, {"hi", 3}, {"hello", 5}, {"c++", 2}, {"java", 6}};
```

`map` 의 경우도 비슷합니다. `map` 의 경우 `vector` 와는 다르게 `pair<Key, Value>` 원소들을 초기화자 리스트의 원소들로 받습니다. `pair` 는 C++ STL 에서 지원하는 간단한 클래스로 그냥 두 개의 원소를 보관하는 객체라고 보시면 됩니다. `map` 에는 `pair` 의 첫 번째 원소로 키를, 두 번째 원소로 값을 전달해주면 됩니다.

### initializer_list 사용 시 주의할 점

생성자들 중에서 `initializer_list` 를 받는 생성자가 있다면 한 가지 주의해야할 점이 있습니다. 만일 `{}` 를 이용해서 객체를 생성할 경우 생성자 오버로딩 시에 해당 함수가 **최우선** 으로 고려된다는 점입니다.

예를 들어서 `vector` 의 경우 아래와 같은 형태의 생성자가 존재합니다.

```cpp-formatted
vector(size_type count);
```

이 생성자는 `count` 개수 만큼의 원소 자리를 미리 생성해놓습니다. 그렇다면

```cpp-formatted
vector v{10};
```

은 해당 생성자를 호출할까요? 아닙니다. 그냥 원소 1 개 짜리 `intializer_list` 라고 생각해서 `10` 을 보관하고 있는 벡터를 생성하게 됩니다.

따라서, 이러한 불상사를 막기 위해서는 `{}` 로 생성하기 보다는 `()` 를 이용해서

```cpp-formatted
vector v(10);
```

과 같이 v 를 생성한다면 우리가 원하는 생성자를 호출할 수 있게 됩니다.

`initializer_list` 를 받는 생성자가 최우선적으로 고려된다는 말은, 컴파일러가 **최선을 다해서** 해당 생성자와 매칭시키려고 노력한다는 의미 입니다. 예를 들어서 아래와 같은 코드를 살펴봅시다.

```cpp-formatted
#include <initializer_list>
#include <iostream>

class A {
 public:
  A(int x, double y) { std::cout << "일반 생성자! " << std::endl; }

  A(std::initializer_list<int> lst) {
    std::cout << "초기화자 사용 생성자! " << std::endl;
  }
};

int main() {
  A a(3, 1.5);  // Good
  A b{3, 1.5};  // Bad!
}
```

컴파일 하였다면 아래와 같은 오류가 발생합니다.

```compiler-warning
test.cc: In function ‘int main()’:
test.cc:15:13: error: narrowing conversion of ‘1.5e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
   A b{3, 1.5}; // Bad!
             ^
```

일단 보시다시피

```cpp-formatted
A a(3, 1.5);  // Good
```

이 문장은 아무런 문제가 없습니다. () 를 이용해서 생성자를 호출하였기 때문에, A 의 첫 번째 생성자인 일반 생성자가 호출됩니다.

하지만 그 다음 문장을 살펴봅시다.

```cpp-formatted
A b{3, 1.5};  // Bad!
```

앞서 C++ 컴파일러는 `{}` 를 이용해서 생성자를 호출하였을 경우 `initializer_list` 를 받는 생성자를 최우선으로 고려한다고 하였습니다. 따라서, 컴파일러는 `initializer_list` 를 이용하도록 최대한 노력하려고 하는데, 1.5 는 `int` 가 아니지만, `double` 에서 `int` 로 암시적 변환을 할 수 있으므로 이를 택하게 됩니다.

그런데 문제는 앞서 `{}` 는 데이터 손실이 있는 변환을 할 수 없다고 하였습니다. 그런데 `double` 에서 `int` 로의 타입 변환은 데이터 손실이 있는 변환이므로, 오류가 발생하게 됩니다. 사실 `A(int x, double y)` 이 생성자가 좀 더 나은 매칭이지만, C++ 컴파일러는 `initializer_list` 를 이용한 생성자를 최대한 고려하려고 합니다. 이러한 문제가 발생하지 않는 경우는 `initializer_list` 의 원소 타입으로 타입 변환 자체가 불가능한 경우여야만 합니다.

```cpp-formatted
#include <initializer_list>
#include <iostream>
#include <string>

class A {
 public:
  A(int x, double y) { std::cout << "일반 생성자! " << std::endl; }

  A(std::initializer_list<std::string> lst) {
    std::cout << "초기화자 사용 생성자! " << std::endl;
  }
};

int main() {
  A a(3, 1.5);        // 일반
  A b{3, 1.5};        // 일반
  A c{"abc", "def"};  // 초기화자
}
```

성공적으로 컴파일 하였다면

```exec
일반 생성자! 
일반 생성자! 
초기화자 사용 생성자! 
```

와 같이 잘 나옵니다. 위 경우 `int` 나 `double` 이 `string` 으로 변환될 수 없기 때문에 `initializer_list` 를 받는 생성자는 아예 고려 대상에서 제외됩니다.

### initializer_list 와 auto

만일 `{}` 를 이용해서 생성할 때 타입으로 `auto` 를 지정한다면 `initializer_list` 객체가 생성됩니다. 예를 들어서

```cpp
auto list = {1, 2, 3};
```

을 하게 되면 `list` 는 `initializer_list<int>` 가 되겠지요.

그렇다면 아래는 어떨까요? [참고로 이 예제는 여기에서 가져왔습니다.](https://mariusbancila.ro/blog/2017/04/13/cpp17-new-rules-for-auto-deduction-from-braced-init-list/)

```cpp
auto a = {1}; // std::initializer_list<int>
auto b {1}; // std::initializer_list<int>
auto c = {1, 2}; // std::initializer_list<int>
auto d {1, 2}; // std::initializer_list<int>
```

상식적으로 적어도 `b` 는 `int` 로 추론되어야 할 것 같지만, C++ 11 에서는 위 `a`, `b`, `c`, `d` 모두 `std::initializer_list<int>` 로 정의됩니다. 

하지만 이는 꽤 비상식적이기 때문에 C++ 17 부터 아래와 같이 두 가지 형태로 구분해서 `auto` 타입이 추론됩니다.

* `auto x = {arg1, arg2...}` 형태의 경우 `arg1`, `arg2` ... 들이 모두 같은 타입이라면 `x` 는 `std::initializer_list<T>` 로 추론됩니다.
* `auto x {arg1, arg2, ...}` 형태의 경우 만일 인자가 단 1 개라면 인자의 타입으로 추론되고, 여러 개일 경우 오류를 발생시킵니다.

따라서 C++ 17 부터는 아래와 같습니다.

```cpp
auto a = {1}; // 첫 번째 형태이므로 std::initializer_list<int>
auto b {1}; // 두 번째 형태 이므로 그냥 int
auto c = {1, 2}; // 첫 번째 형태이므로 std::initializer_list<int>
auto d {1, 2}; // 두 번째 형태 인데 인자가 2 개 이상이므로 컴파일 오류
```

좀 더 직관적으로 바뀐 것을 알 수 있습니다.

유니폼 초기화와 `auto` 를 같이 사용 할 때 또 한 가지 주의할 점은, 문자열을 다룰 때

```cpp
auto list = {"a", "b", "cc"};
```

를 하게 된다면 `list` 는 `initializer_list<std::string>` 이 아닌 `initializer_list<const char*>` 이 된다는 점입니다. 물론 이 문제는 C++ 14 에서 추가된 리터럴 연산자를 통해 해결할 수 있습니다.

```cpp
using namespace std::literals; // 문자열 리터럴 연산자를 사용하기 위해 추가해줘야함.
auto list = {"a"s, "b"s, "c"s};
```

와 같이 하면, `initializer_list<std::string>` 으로 추론됩니다.

자 그럼 이것으로 이번 강좌를 마무리 하도록 하겠습니다. 다음 강좌에서는 C++ 11 에서 추가된 `constexpr` 와 가변 길이 템플릿에 대해 다루어보도록 하겠습니다.

```lec-summary
유니폼 초기화 (`{}` 를 이용한 생성자 호출) 를 통해서 인자 없는 생성자가 함수의 정의로 오해되는 일을 막을 수 있으며 `initializer_list` 를 만들어 전달할 수 있습니다.
`initializer_list` 를 통해서 객체를 간단하게 생성할 수 있습니다.
```

##@ chewing-cpp-end