Post List

2015년 4월 17일 금요일

item 07: uniform initializer 및 std::initializer_list에 대하여 알아봅시다. (MVA Version)

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








Effective Modern C++ - Scott Meyers
Item 7: Distinguish between () and {} when creating objects.

Item 7 Object를 생성할 때 ()와 {}를 구분하라.

가 원제이지만, 제 나름대로의 제목으로는

uniform initializer 및 std::initializer_list에 대하여 자세히 알아봅시다.

로 정했습니다.

다룰 내용에 대해서는

1. Object Initializing (개체 초기화)

2. Uniform Initilizer 소개 및 특징

3. std::initializer_list 생성자

4. template내부에서의 object 생성

이렇게 크게 4가지로 나눠서 말씀드리겠습니다.

1. Object Initializing

1.1 일반 개체 초기화 

일반개체를 초기화하는 방법은 4가지가 있습니다.

int x(0);              // initializer is in parentheses
int y = 0;             // initializer follows '='
int z{0};              // initializer is in braces
int z = { 0 };         // initializer is in braces with '='

통상적으로 { } 와 =  { } 는 똑같이 취급을 합니다.
그래서 초기화 방법으로는 3가지가 있는 것으로 하고 진행하겠습니다.
( 괄호 ( ) , 중괄호 { }, 대입 = )

하지만  { } 와 =  { } 를 다르게 하자는 의견도 있습니다.
auto와 { } 를 같이 사용해서 초기화할 경우 std::initializer_list로 추론이 되어서 애매할 경우가 있습니다.
여기에 대해서 MS 김명신 부장님의 blog에서 다룬 내용이 있습니다.
( C++11 auto와 {}-lini-list 의 모호함 - 김명신의 즐거운 하루 )
N3922(http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2014/n3922.html)의 표준화에 대한 내용인데,
지금은 표준으로 채택 되었습니다.

1.2 non-static member variable 초기화

C++11부터는 class 선언시 non-static member variable의 초기값을 바로 넣을 수가 있습니다.

class Widget
{
    ...
private:
    int x{0};              // fine. x's default value is 0
    int y = 0;             // also fine
    int z(0);              // error!
};

초기화 방법중 중괄호 { } 와 대입 = 는 쓸 수 있지만 괄호 ( ) 는 사용을 못합니다.

1.3 COPY 불가 개체 초기화

C++에서 대표적인 COPY 불가 개체로는 std::atomic을 예로 들 수 있습니다.

std::atomic<int> ai1{0};              // fine
std::atomic<int> ai2(0);              // fine
std::atomic<int> ai3 = 0;             // error!

COPY 불가 개체의 초기화에는 중괄호 { } 와 괄호 ( ) 는 쓸 수 있지만 대입 = 은 쓸 수 없습니다.

Quiz ! 앞에 본 3가지 경우에 대해서 모두 다 사용 가능한 초기화 방법은 무엇일까요 ?

정답은 바로 중괄호 { } 를 이용한 초기화 방법입니다.
모든 곳에서 중괄호 { } 를 사용한 초기화는 다 사용이 가능합니다.
중괄호 { } 를 사용하는 초기화 방법을 Uniform Initializer 라고 부릅니다.


2. Uniform Initializer

- 모든 경우에 대해서 초기화가 가능합니다.
   (이미 위에서 예제를 살펴보았습니다.)

STL Container (e.g. std::vector<int>) 에서 내부값 (1,3,5...)과 함께 초기화하는 것이 가능합니다.
  (과거에는 불가능했습니다.)

std::vector<int> v{ 1, 3, 5 };        // v's initial content is 1, 3, 5

bulit-in type 사이에서 암시적 narrowing conversion (표현 범위가 큰 타입에서 작은 타입으로의 대입) 을 금지하고 있습니다.
  wide conversion은 지원하고 있습니다.
double x, y, z;
...
int sum1{ x + y + z }; // error C2397 : conversion from double to int requires a narrowing conversion

괄호 ( ) 와 대입 = 를 사용한 초기화는 narrowing conversion을 체크하지 않습니다.
왜냐면 기존에 작성된 프로그램들 (legacy code)에서 너무 많이 쓰여졌기 때문이다.

int sum2(x + y + z);   // okay (value of expression truncated to an int)
int sum3 = x + y + z;  // ditto

 most vexing parse를 야기하지 않는다는 점입니다.
   (most vexing parse에 대한 자세한 내용은 item 6. C++의 most vexing parse 를 조심하자.를 참조)
   간단히 설명하자면, 인자없는 초기화에서 괄호 ( ) 를 사용하면, 생성자가 호출되는게 아니라,
   해당 타입을 return 하는 함수를 선언하는 것으로 컴파일러가 인식하는 경우를 말합니다.

Widget w1(10);  // call Widget ctor with argument 10
Widget w2();    // most vexing parse! declares a function named w2 that returns a Widget!
Widget w3{};    // calls Widget ctor with no args

- 중괄호 { } 를 이용한 초기화는 내부적으로 std::initializer_list를 인자로 생성하도록 되어 있습니다.
  std::initializer_list는 built-in type 및 STL의 Container 로 암묵적으로 변환이 가능하도록 설계되어 있습니다.


3. std::initializer_list 생성자

class의 생성자에서 인자로 std::initializer_list 를 받는 것을 std::initializer_list 생성자라고 합니다.
std::initializer_list 생성자가 없는 경우에는 개체 초기화에서 괄호 ( ) 를 사용한 것과 중괄호 { } 를 사용한 것은 같은 의미입니다.

class Widget
{
public:
    Widget(int i, bool b);   // ctors not declaring
    Widget(int i, double d); // std::initializer_list params
};

Widget w1(10, true);         // calls first ctor
Widget w2{10, true};         // also calls first ctor
Widget w3(10, 5.0);          // calls second ctor
Widget w4{10, 5.0};          // also calls second ctor

하지만, std::initializer_list 생성자가 있는 경우 conversion이 불가능한 경우를 제외하고는 무조건 std::initializer_list 생성자가 호출됩니다.
예를 들어서  std::initializer_list<long double>을 매개변수로 받는 생성자를 추가할 경우를 한번 보겠습니다.

class Widget
{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<long double> il); // added
};

Widget w2{ 10, true }; // 10 and true convert to long double
Widget w4{ 10, 5.0 };  // 10 and 5.0  convert to long double

원래 예제에서 잘 생성되었던 w2, w4가 이제는 std::initializer_list<long double>생성자를 호출하게 됩니다.
문제는 그것만이 아닙니다. COPY, MOVE 생성자도 std::initializer_list 생성자가 가로채게 됩니다.

class Widget
{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<long double> il);

    operator float() const// convert to float
};

Widget w5(w4);              // used parens, calls copy ctor
Widget w6{ w4 };            // w4 converts to float, and float converts to long double

Widget w7(std::move(w4));   // used parens, calls copy ctor
Widget w8{ std::move(w4) }; // same as w6

중괄호 { } 를 사용한 초기화는 무조건 std::initializer_list 생성자를 부르려 합니다.
심지어 다른 생성자에서는 정상적으로 동작하고, std::initializer_list 생성자에서는 컴파일 에러를 일이키는 경우에서도 말입니다.

class Widget {
public:
        Widget(int i, bool b);
        Widget(int i, double d);
        Widget(std::initializer_list<bool> il); // bool 바꾸고, conversion 함수를 제거.
};

Widget w{ 10, 5.0 };  // error! invalid narrowing conversion from 'double' to 'bool'

Compiler는 첫 2가지 생성자를 무시합니다.
(심지어 두번째꺼는 매개변수 타잎이 완전 일치합니다.)
그리고 어떻해서든 std::initializer_list<bool>을 사용할려고 노력합니다.
그렇게 할려면 int(10), double(5.0)을 bool로 변환을 해야하는데, narrowing conversion은 안되므로, 에러가 발생합니다.

타입 변환의 방법이 완전히 없는 경우에만  std::initializer_list 생성자 호출을 포기합니다.

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<std::string> il); // std::string 바꾸고, conversion 함수를 제거
    ...
};

Widget w1(10, true);    // use parens, still calls first  ctor
Widget w2{10, true};    // use braces,   now calls first  ctor
Widget w3(10, 5.0);     // use parens, still calls second ctor
Widget w4{10, 5.0};     // use braces,   now calls second ctor

 std::initializer_list<std::string>에 대해서는 int와 bool을 std::string로 변환시킬 방법이 없으므로,
정상적인 다른 생성자를 호출한다.

그럼 아무 인자가 없는 경우에 대해서 중괄호 { } 를 사용하면 어떻게 될까요 ?
이 경우에는 기본 생성자가 호출됩니다.
인자 없이 std::initializer_list 생성자를 호출 하려면 인자로 { } 를 넣어줘야 합니다.

class Widget {
public:
    Widget();
    Widget(std::initializer_list<int> il);
};

Widget w1;     //      calls default ctor
Widget w2{};   // also calls default ctor
Widget w3();   // most vexing parse! declares a function!
Widget w4({}); // calls std::initializer_list ctor with empty list
Widget w5{{}}; // ditto

중괄호 { } 초기화와 std::initializer_list가 기존 생성자들과의  관계에서 문제를 일으키는 대표적인 예로
std::vector가 있습니다.
std::vector는 std::initializer_list를 사용하지 않는 생성자가 있습니다.
2개의 인자를 받으며, 첫번째 인자 개수만큼 2번째 인자값으로 채워줍니다.
만약 std::vector<int를 생성할 때 괄호 ( )를 쓰느냐 중괄호 { }를 쓰느냐에 따라서 어마어마한 차이를 보입니다.

std::vector<int> v2(10, 20);  // use non-std::initializer_list ctor
                              // : create 10-element, all elements have value of 20

std::vector<int> v2{10, 20};  // use     std::initializer_list ctor
                              // : create  2-element, element values are 10 and 20

class를 정의할 때 std::initializer_list생성자를 만들기 전에 충분히 생각을 해야 합니다.
특히 기존에 이미 있던 class에 std::initializer_list생성자를 추가하는 것은 더더욱 피해야 합니다.
std::initializer_list생성자를 추가했을 때 기존에 있던 원래 생성자를 호출할 방법이 있는지를 먼저 생각해야 합니다.


4. template 내부에서의 object 생성

괄호 ( ) 와 중괄호 { } 중 개체를 초기화 할때  default로 뭐를 써야 할까요 ?
보통 그중 하나를 그냥 쓸 것이고, 다른 하나는 특별한 경우만 쓸것입니다.
중괄호 { }를 default로 쓴다면 narrowing conversion과 most vexing parse를 막을 수 있습니다.
하지만 std::vector를 예에서 본 것처럼 괄호 ( ) 와 중괄호 { } 가 다르게 동작하는 경우에 대해서는 좀 더 생각을 해 줘야 합니다.

template생성시에 괄호 ( ) 와 중괄호 { } 를 이용하여 object를 만들때 특히 더 주의해야 합니다.
예를 들어 임의의 object를 생성하는 가변인자 template이 있을 경우

template<typename T,      // type of object to create
         typename... Ts>  // type of arguments to use
void MakeAndFill(Ts&&... params)
{
    create local T object from params...
}

위의 psudo-code 부분을 실제 code로 적는데는 2가지 방법이 있습니다.

T localObject(std::forware<Ts>(params)...);  // use ()

T localObject{std::forward<Ts>(params)...};  // use {}

아래와 같은 code가 실행될 때로는

std::vector<int> v;
...
MakeAndFill<std::vector<int>>(10, 20);

괄호 ( )   를 사용하여 localObject를 생성한 경우는 10개의 element를 가지는 std::vector가 생성될 것이고,
중괄호 { }를 사용하여 localObject를 생성한 경우는  2개의 element를 가지는 std::vector가 생성될 것입니다.

뭐가 맞는 걸까 ? MakeAndFill()을 만든 사람은 알수가 없습니다.
해당 함수를 호출하는 사람만 알 수 있겠지요.

이것은 std::make_unique나 std::make_shared 같은  Standard Library function들이 직면한 문제이기도 합니다.
이런 함수들은 내부적으로 괄호 ( )를 사용한다고 문서화하여 문제를 해결했습니다.

Things to Remember

* 중괄호 { } 초기화는 가장 보편적으로 사용이 가능한 초기화입니다.
  narrowing conversion과 most vexing parse를 막아줍니다.

* 생성자들 중에서 중괄호 { } 초기화는 더 완벽해보이는 다른 생성자가 있음에도 불구하고 가능한한 std::initializer_list를 호출하고자 합니다.

* 괄호 ( ) 와 중괄호 { } 중 뭐를 선택하느냐에 따라 다른 결과가 생성되는 예로
   std::vector<numeric type>>을 2개의 인자로 생성하는 경우가 있습니다.

template내에서 객체 생성시 괄호 ( )와 중괄호 { } 중 뭐를 선택하냐는 것은 심사숙고해야 합니다.

* MVA용 Slide



댓글 없음:

댓글 쓰기