[Effective Modern C++] 항목 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하라
객체를 생성하는 방법은 생각보다 다양하다. 아래와 같은 방법이 존재한다.
int x(0);
int y = 0;
int z{ 0 };
int z = { 0 };
위의 4가지 방법 모두 똑같은 결과를 나타낸다. 그 중에서 중괄호({})로 나타낸 생성 방법을 바로 균일 초기화(uniform initialization)이라고 한다.
균일 초기화
도입 배경
균일 초기화라고 부르는 이유는 어떤 상황에서든 균일하게 초기화를 할 수 있기 때문이다. 균일 초기화를 도입하게 된 3가지 배경을 먼저 알아보자.
컨테이너 초기화
vector와 같이 배열의 경우 초기 값들을 지정할 수 있다.
std::vector<int> v{ 1, 3, 5 }; // v의 원소는 1, 3, 5
멤버 초기화
non-static 자료 멤버를 초기화 하는데 사용할 수 있다. 소괄호의 경우에는 컴파일 에러가 난다.
class Widget {
...
private:
int x{ 0 };
int y = 0;
int z(0); // error
}
복사 불가 객체 초기화
복사 불가능한 객체 또한 초기화가 가능하다.
std::atomic<int> ai1{ 0 };
std::atomic<int> ai2(0);
std::atomic<int> ai3 = 0; // error
장점
narrowing conversion 방지
균일 초기화의 장점중 하나는 좁히기 변환(narrowing conversion)을 방지해준다. 좁히기 변환이 무엇인지 먼저 알아보자.
double x, y, z;
int sum1(x + y + z};
int sum2 = x + y + z;
위의 결과는 모두가 알다시피 double 에서 int 로 변환을 해준다. 그렇기 때문에 실수로 인해 발생하는 오류를 찾기 힘들어질수도 있다. 하지만 균일 초기화를 사용하면 이런 문제가 없다.
double x, y, z;
int sum1{ x + y + z }; // error
컴파일러가 에러를 보내 이런 실수가 발생하는 것을 방지해준다.
most vexing parse에 자유로움
c++에는 가장 성가신 구문 해석(most vexing parse)라는 것이 있다. 인수가 없는 생성자를 호출하려고 할 때 컴파일러가 함수명인지 변수명 구분하기가 어려워 발생한다.
Widget w1(); // w1이라는 함수 선언
Widget w2{}; // 인수 없는 생성자 호출
위 예제에서 보듯이 균일 초기화에서는 문제없이 생성자를 호출해준다.
단점
위와 같은 장점들이 있지만 단점 또한 존재한다.
std::initializer_list 문제
생성자 중에 std::initailizer_list 를 파라미터로 사용하는 경우에 예상치 못한 문제가 발생한다.
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);
}
Widget w1(10, true); // Widget(int i, bool b) 호출
Widget w1{10, true}; // Widget(std::initializer_list<long double> il) 호출
Widget w1(10, 5.0); // Widget(int i, double d) 호출
Widget w1{10, 5.0}; // Widget(std::initializer_list<long double> il) 호출
명백히 타입이 맞지 않음에도 long double 로 변환되어서 호출한다.
물론 회피 가능하도록 작성이 가능하다.
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(int i, bool b) 호출
Widget w1{10, true}; // Widget(int i, bool b) 호출
Widget w1(10, 5.0); // Widget(int i, double d) 호출
Widget w1{10, 5.0}; // Widget(int i, double d) 호출
위와 같이 narrowing conversion을 할 수 없는 경우(int -> string) 에는 의도한대로 잘 동작한다.
컨테이너 초기화
std::initializer_list는 컨테이너를 초기화하는데 사용한다. 그런데 만약 빈 컨테이너를 만들고 싶다면 어떻게 해야할까?
Widget w1{{}};
Widget w2({});
위와 같이 중괄호를 괄호로 감싸서 선언해주기만 하면된다.
마무리
앞서 살펴봤듯이 균일 초기화를 항상 써야하는 것은 아니다. 적절한 상황에 맞춰 사용하는 것이 좋다. 대표적으로 vector 객체를 생성해보자.
std::vector<int> v1(10, 20); // 20으로 초기화된 10개의 원소
std::vector<int> v2{10, 20}; // 2개의 원소
v1과 v2는 똑같은 파라미터를 사용하고 있지만 그 결과는 아주 다르다. 두 방법의 차이를 익히고 적재적소에 사용하는 것이 좋겠다.
참고 서적
스콧 마이어스, Effective Modern C++