Post List

2015년 3월 27일 금요일

item 07. Object 생성시 ( )와 { }를 구분하라. (Study PT Version)

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








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

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

Object 초기화할 때 아래와 같이 할 수 있다.

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



이 item을 진행하면서 equal + {} 문법을 무시하겠다. 왜냐면 {}만 쓴거랑 같이 처리되기 때문이다.

(하지만 Visual Studio에서는 다르게 처리된다. C++11 auto와 {}-lini-list 의 모호함 - 김명신의 즐거운 하루 참조)

C++ 초보자들에게 = 는 공간을 할당하는 명령어로 오해되기 쉽지만, 사실은 그렇지 않다.
int 와 같은 built-in type에 있어서 할당에서 초기화를 구분하는 것은 중요하다. 왜냐면 다른 함수가 호출되기 때문이다.

Widget w1;             // call default constructor
Widget w2 = w1;        // not an assignment; calls copy ctor
w1 = w2;               // an assignment; calls copy operator =



몇가지 초기화 문법들은 C++98에서는 불가능했던 것들도 있다. 예를 들어서 STL Container (std::vector<int>) 에서 내부값 (1,3,5...)과 함께 초기화하는 것은 불가능했다.

초기화 문법이 여러개 있었다는 것에서, 모든 초기화 상황에 대해서 다 커버를 할 수 없었다는 것을 알수 있다.
C++11에는 uniform initialization 가 있다.
(하나의 초기화 문법으로 모든 경우에 다 사용한다. {}를 사용한다. (그래서 스캇 아저씨는 이것을 braced initialization 이라고 부른다고 한다.)

이것은 과거에는 불가능했던 것을 쉽게 사용할수 있게 해주었다.

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



{}는 non-static value에 대한 default 초기값을 설정하는데도 쓰인다. = 를 써도 된다. 그런데 ()는 안된다.

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



그러나 copy가 안되는 object에 대해서는 ()는 되는데, =는 안된다.

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



이제 왜 {}를 이용한 초기화를 uniform 이라고 부르는지 알 것이다. C++에는 초기화를 하는 방법에 3가지가 있다. ( {}, (), = ). {} 만 모든 경우에 다 사용이 가능하다.

{} 초기화의 새로운 기능은 bulit-in type 사이에서 암시적 narrowing conversion을 금지하고 있다는 점이다.
{} 초기화되는 object의 type이 다를 경우, Code는 컴파일 되지 않을 것이다.

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



또 다른 {} 초기화의 주목할만한 점은 C++의 most vexing parse를 야기하지 않는다는 점입니다.

(most vexing parse에 대한 자세한 내용은 item 6. C++의 most vexing parse 를 조심하자.를 참조)

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



{} 초기화에 대한 예제는 많이 있다. 암시적 narrowing conversion과 most vexing parse를 방지 등의 다양한 상황에 사용된다.

하지만 {} 초기화에도 약점은 있다. 가끔씩 {} 초기화, std::initializer_list, 생성자 들 사이에서 얽히는 경우가 있다.

생성자의 경우 std::initializer_list 를 parameter로 받는 것을 생성하지 않았다면 () 와 {} 는 같은 의미이다.

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를 parameter로 받는 생성자가 추가 되면, 컴파일러는 {} 초기화는 최대한 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
        ...
};



이럴 경우 w2, w4는std::initializer_list를 사용하는 생성자를 사용하게 된다.

Widget w2{ 10, true }; // 10 and true convert to long double
Widget w4{ 10, 5.0 };  // 10 and 5.0 convert to 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은 {} 초기화에서 금지되므로, 이 Code는 정상적으로 동작하지 않는다.

{} 초기화 안의 매개변수를 std::initializer_list의 Type으로 변환할 수 있는 방법이 완전히 없는 경우만 정상적으로 다른 생성자를 부른다.
예를 들어서 std::initializer_list<bool>을 std::initializer_list<std::string>로 바꿀경우 int와 bool을 std::string로 변환시킬 방법이 없으므로,
정상적인 다른 생성자를 호출한다.

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



{} 초기화 안에 아무런 인자 없이 사용하면 default 생성자와 std::initializer_listt 생성자중 뭐가 호출 될까 ?
인자가 없다는 것으로 해석된다면 default 생성자가 호출되는게 맞겠고,
std::initializer_list를 empty로 보내는 것이라고 해석된다면 std::initializer_list 생성자가 호출되는게 맞다.
정답은 default 생성자가 호출된다.

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!



std::initializer_list생성자에 를 empty 상태의 인자로 호출을 하고 싶으면 empty 상태의 {}를 인자로 넣어주면 된다.

Widget w4({});  // calls std::initializer_list ctor with empty list
Widget w5{{}};  // ditto



{} 초기화, std::initializer_list, 생성자 overloading 들에 대한 이런 관계들이 우리가 프로그래밍을 하는데 얼마나 문제를 일으킬까 라고 생각을 할수 있겟지만,
생각보다 직접 영향을 받는 class는 std::vector이다.
std::vector는 std::initializer_list를 사용하지 않는 생성자가 있다. 인자로 초기 size와 그 만큼 특정 초기값으로 채워준다.
그런데 std::vector에는 std::initializer_list 생성자도 가지고 있어서 특정 값들로 초기화를 시킬수가 있다.
만약 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를 정의할 때 다음 2가지 경우에 대해서 충분히 생각을 해야 한다.

첫째는 class를 디자인 할때 생성자를 만들떄 {} 생성자를 만들때 심사숙고해라.
만약 {}생성자가 없어서 새로 만들었을 때 기존의 생성자들이 필요한 경우 어떻게 호출할 수 있는지 그 방법을 강구해야 한다.

두번째, () 와 {} 생성자를 조심스럽게 선택해서 써야한다.많은 개발자들이 그중 하나를 default로 그냥 쓸 것이고, 다른 하나는 특별한 경우만 쓸것이다.
{}를 default로 쓴다면 narrowing conversion과 most vexing parse를 막을 수 있다.
std::vector를 예로 들자면 크기와 초기값으로 생성하고자 할때는 ()를 사용하고, 각각의 초기값으로 생성하고 할땐 {}초기화를 써야한다.
분명 둘 사이에는 차이가 있는 type들이 있으므로 한가지 기준을 정해서 일관되게 적용하는 것이 좋다.

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

template<typename T,      // type of object to create
         typename... Ts>  // type of arguments to use
void doSomeWork(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;
...
doSomeWork<std::vector<int>>(10, 20);



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

뭐가 맞는 걸까 ? doSomeWork를 만든 사람은 알수가 없다. 해당 함수를 호출하는 사람만 알 수 있다.

이것은 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내에서 객체 생성시 ()와 {} 중 뭐를 선택하냐는 것은 심사숙고해봐야 한다.


댓글 없음:

댓글 쓰기