Uniform initialization

Modern C++ 을 사용하면서 쉽게 지나치기 쉬운 새로운 기능들
중괄호 초기화


객체 초기화 방식

C++11에서 객체를 초기화하는 방법

1
2
3
4
int x( 0 ); 
int y = 0;
int z{ 0 };
int w = { 0 }; // 대체로 int w{ 0 }; 와 같음

객체의 초기화 과정에서 '='를 사용하는 초기화는 할당연산자(assignment operator)가 아닌 복사생성자(copy constructor)를 호출한다.


Uniform Initialization

C++11에서 중괄호({})를 이용한 균일초기화를 도입하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1, 3, 5 요소를 갖는 벡터
std::vector<int> v{ 1, 3, 5 };

// non-static member variable 초기화
class Widget
{
...
private:
int x{ 0 }; // OK
int y = 0; // OK
int z( 0 ); // ERROR
};

// non-copyable object 초기화
std::atomic<int> ai1{ 0 }; // OK
std::atomic<int> ai2( 0 ); // OK
std::atomic<int> ai3 = 0; // ERROR

균일초기화는 C++에서 지원하는 세 가지 초기화 표현식(=, (), {}) 중 유일하게 어디든 사용할 수 있다.


균일초기화는 implicit narrowing conversion을 방지한다.

1
2
3
4
5
double x, y, z;
...
int sum1{ x + y + z }; // ERROR
int sum2( x + y + z ); // OK, but implicit narrowing conversion
int sum3 = x + y + z; // sum2와 같음

균일초기화는 most vexing parse에서 자유롭다.

1
2
3
4
5
6
7
Widget w1(10); // Widget의 생성자 호출

Widget w2(); // most vexing parse.
// Widget의 기본 생성자를 호출하는 것이 아닌
// Widget 객체를 반환하는 함수를 선언

Widget w3{}; // 인수가 없는 Widget의 생성자 호출

균일초기화는 std::initializer_list를 매개변수로 하는 생성자를 우선으로 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
...
};

Widget w1( 10, true ); // 첫 번째 생성자 호출
Widget w2{ 10, true }; // 첫 번째 생성자 호출
Widget w3( 10, 5.0 ); // 두 번째 생성자 호출
Widget w4{ 10, 5.0 }; // 두 번째 생성자 호출
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
26
27
28
29
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);
operator float() const;
...
};

Widget w1( 10, true ); // 첫 번째 생성자 호출

Widget w2{ 10, true }; // 세 번째 생성자 호출
// 10과 true가 long double로 변형됨

Widget w3( 10, 5.0 ); // 두 번째 생성자 호출

Widget w4{ 10, 5.0 }; // 세 번째 생성자 호출
// 10과 5.0이 long double로 변형됨

Widget w5( w4 ); // 복사 생성자 호출

Widget w6{ w4 }; // 세 번째 생성자 호출
// w4가 float으로 변형되고 다시
// long double로 변형됨

Widget w7( std::move(w4) ); // 이동 생성자 호출

Widget w8{ std::move(w4) }; // w6과 똑같음

다른 적절한 생성자가 있어도 std::initializer_list를 인자로 하는 생성자가 있다면 해당 생성자를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<bool> il);
...
};

Widget w{ 10, 5.0 }; // ERROR
// 10과 5.0이 bool로 변경되려고 하지만,
// 균일초기화는 implicit narrowing conversion을 방지한다.

균일초기화의 인수 형식들을 std::initializer_list안의 형식으로 변환하는 방법이 아예 없을때만 다른 생성자를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<std::string> il);
...
};

Widget w1{ 10, true }; // 첫 번째 생성자 호출
Widget w2{ 10, 5.0 }; // 두 번째 생성자 호출

중괄호안의 인수들을 std::string으로 변환할 수 없으므로 이 경우에는 세 번째 생성자는 무시된다.


빈 중괄호 쌍은 빈std::initializer_list가 아닌 인수 없음을 뜻한다.

1
2
3
4
5
6
7
8
9
class Widget
{
public:
Widget();
Widget(std::initializer_list<int> il);
...
};

Widget w{}; // 기본 생성자 호출

비어있는 std::initializer_list를 인수로하는 std::initializer_list생성자를 호출하려면 중괄호를 중괄호를 감싼다.

1
2
Widget w1( {} ); // 두 번째 생성자 호출
Widget w2{ {} }; // 두 번째 생성자 호출

템플릿의 경우 선언시점에 {}()중 어떤것을 사용해야 하는지 판단할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T, typename ...Args>
void doSomething(Args&& ...args)
{
/* args로부터 T를 생성한다. */
T localObj1( std::forward<Args>(args)... ); // 괄호
T localObj2{ std::forward<Args>(args)... }; // 중괄호
}

std::vector<int> v;
...

doSomething<std::vector<int>>(10, 20);

doSomthinglocalObj1은 값이 20인 요소 10개를 갖는 벡터이고, localObj2는 값이 10, 20인 2개의 요소를 갖는 벡터이다. 따라서 위와 같은 템플릿을 작성하는 경우 해당 내용을 문서화 해야 한다.


결론

  • 클래스를 작성할 때 std::initializer_list를 받는 생성자를 추가한다면, 클라이언트 코드가 해당 생성자를 의도치 않게 호출할 수 있다는 것을 생각해야 한다.
  • 객체를 초기화할 때 괄호와 중괄호의 차이점을 알고 선택해야 한다.
  • 템플릿을 작성할 때 괄호와 중괄호에 따라 결과가 달라진다면, 내부 구현에대해서 문서화할 필요가 있다.

References

Author

Joyus.Gim

Posted on

2022-06-18

Updated on

2022-07-19

Licensed under

Comments