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 |
(하지만 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++11에는 uniform initialization 가 있다.
(하나의 초기화 문법으로 모든 경우에 다 사용한다. {}를 사용한다. (그래서 스캇 아저씨는 이것을 braced initialization 이라고 부른다고 한다.)
이것은 과거에는 불가능했던 것을 쉽게 사용할수 있게 해주었다.
std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5 |
class Widget
{
...
private:
int x{0}; // fine. x's default value is 0
int y = 0; // also fine
int z(0); // error!
};
|
std::atomic<int> ai1{0}; // fine
std::atomic<int> ai2(0); // fine
std::atomic<int> ai3 = 0; // error!
|
{} 초기화의 새로운 기능은 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
|
int sum2(x + y + z); // okay (value of expression truncated to an int)
int sum3 = x + y + z; // ditto
|
(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
|
하지만 {} 초기화에도 약점은 있다. 가끔씩 {} 초기화, 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<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
|
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
|
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'
|
그렇게 할려면 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_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!
|
Widget w4({}); // calls std::initializer_list ctor with empty list
Widget w5{{}}; // ditto
|
생각보다 직접 영향을 받는 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를 디자인 할때 생성자를 만들떄 {} 생성자를 만들때 심사숙고해라.
만약 {}생성자가 없어서 새로 만들었을 때 기존의 생성자들이 필요한 경우 어떻게 호출할 수 있는지 그 방법을 강구해야 한다.
두번째, () 와 {} 생성자를 조심스럽게 선택해서 써야한다.많은 개발자들이 그중 하나를 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...
...
}
|
T localObject(std::forware<Ts>(params)...); // use ()
T localObject{std::forward<Ts>(params)...}; // use {}
|
std::vector<int> v;
...
doSomeWork<std::vector<int>>(10, 20);
|
{}를 사용하여 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내에서 객체 생성시 ()와 {} 중 뭐를 선택하냐는 것은 심사숙고해봐야 한다. |
댓글 없음:
댓글 쓰기