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!
|
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 (표현 범위가 큰 타입에서 작은 타입으로의 대입) 을 금지하고 있습니다.
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를 야기하지 않는다는 점입니다.
간단히 설명하자면, 인자없는 초기화에서 괄호 ( ) 를 사용하면, 생성자가 호출되는게 아니라,
해당 타입을 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는 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 생성자가 호출됩니다.
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'
|
(심지어 두번째꺼는 매개변수 타잎이 완전 일치합니다.)
그리고 어떻해서든 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가 있습니다.
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
[C++ Korea] Effective Modern C++ MVA item 7 Distinguish between ( ) and { } when creating objects from Seok-joon Yun
댓글 없음:
댓글 쓰기