본문 바로가기
C++/Effective Modern C++

객체 생성시 괄호와 중괄호의 구분

by COCO1337 2020. 6. 29.

C++에서는 초기화 값을 괄호, 등호, 중괄호로 지정할 수 있고 등호와 중괄호를 함께 사용할 수 있는 경우도 많다.

int x(0);		// 초기치를 괄호로 감싼 예
int y = 0;		// 초기치를 "=" 다음에 지정한 예
int z{ 0 };		// 초기치를 중괄호로 감싼 예
int t = { 0 };		// "="와 중괄호로 초기치를 지정한 예

대체로 C++은 등호와 중괄호 구문은 중괄호만 사용한 구문과 동일하게 취급한다.

 

int같은 내장 형식에서 초기화와 배정은 학술정긴 차원에서만 차이가 나지만 사용자 정의 형식에서는 초기화와 배정이 각자 다른 함수들을 호출하기 때문에 둘을 구분해야 한다.

Widget w1;		// 기본 생성자를 호출	
Widget w2 = w1;		// 배정이 아님, 복사 생성자를 호출
w1 = w2;		// 배정, 혹사 배정 연산자(operator=) 호출

여러 초기화 구문이 주는 혼동을 완화하고, 그 구문들이 모든 가능한 초기화 시나리오를 포괄하지 않는다는 사실을 해결하기 위해, C++11에서는 균일 초기화를 도입했다. 균일 초기화 구문은 중괄호를 사용하며, 여기에서는 중괄호로 감싼 초기화, 줄여서 중괄호 초기화라고 할 것이다.


중괄호 초기화를 이용하면 컨테이너의 초기 내용을 중괄호를 이용해 지정하거나

std::vector<int> v{1, 3, 5};	// v의 초기 내용은 1, 3, 5

비정적 자료 멤버의 기본 초기화 값을 지정하는데에도 사용할 수 있다.

class Widget {
...
private:
	int x{ 0 };		// x의 기본값은 0
	int y = 0;		// y의 기본값은 0
	int z(0);		// 오류
};

반면, 복사할 수 없는 객체는 중괄호나 괄호로 초기화 할 수 있지만 "="로는 초기화 할 수 없다.

std::atomic<int> ai1{ 0 };		// ok
std::atomic<int> ai2(0);		// ok
std::atomic<int> ai3 = 0;		// 오류

위 세가지를 모두 사용할 수 있기 때문에 '균일'초기화 라고 불린다.

 

중괄호 초기화의 혁신적인 기능 하나는, 내장 기능들 사이의 암묵적 좁히기 변환(narrowing conversion)을 방지해 준다는 것이다. 중괄호 초기치에 있는 어떤 표현식의 값을 초기화 하려는 객체의 형식으로 온전하게 표현할 수 있음이 보장되지 않는 경우, 컴파일러는 반드시 그 사실을 보고해야 한다.

하지만 "="나 괄호를 이용한 초기화는 이러한 좁히기 변환을 점검하지 않는다.

double x, y, z;
int sum1{x + y + z};		// 오류! double 들의 합으로 int를 표현하지 못할 수 있음
int sum2(x + y + z);		// 표현식의 값이 int에 맞게 잘려나감
int sum3 = x + y + z;		// 위와 동일

중괄호 초기치는 C++에서 가장 성가신 구문 해석(most vexing parse)에 자유롭다. 가장 성가신 구문 해석은 "선언으로 해석할 수 있는 것은 항상 선언으로 해석해야 한다"는 C++의 규칙에서 비롯된 하나의 부작용이다.

이에 대한 예로

Widget w1(10);		// 인수 10으로 Widget의 생성자를 호출
Widget w2();		// 가장 성가신 구문 해석! Widget을 돌려주는, w2라는 함수 선언
Widget w3{};		// 인수 없이 Widget의 생성자 호출

하지만 매개변수 목록을 중괄호로 감싸서 함수를 선언할 수는 없으므로, 중괄호를 이요해서 객체를 기본 생성할 때에는 이런 문제를 겪지 않는다.


중괄호 초기화의 단점은 종종 예상치 못한 행동을 보인다는 것이다.

중괄호 초기치, std::initializer_list, 생성자 중복 적재 해소의 상호작용 때문에, 코드가 엉뚱한 일을 하는 것처럼 보이는 현상이 발생한다.

생성자 호출에서 std::initializer_list 매개변수가 관여하지 않는 한 괄호와 중괄호의 의미는 같다.

class Widget {
public:
	Widget(int i, bool b);		// std::initializer_list 매개변수를 선언하지 않는 생성자
	Widget(int i, double d);
...
};

Widget w1(10, true); 		// 첫 생성자 호출
Widget w2{10, true};		// 첫 생성자 호출
Widget w3(10, 5.0);		// 두번째 생성자 호출
Widget w4{10, 5.0};		// 두번째 생성자 호출

하지만 생성자 중 하나 이상이 std::initializer_list 형식의 매개변수를 선언한다면 중괄호 초기화 구문은 std::initializer_list를 받는 중복 적재 버전을 강하게 선호한다. 중괄호 초기치가 쓰인 호출을 std::initializer_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 w2{10, true};		// std::initializer_list 생성자 호출(10과 true가 long double로 변환)
Widget w3(10, 5.0);		// 두번째 생성자 호출
Widget w4{10, 5.0};		// std::initializer_list 생성자 호출(10과 5.0이 long double로 변환)

보통은 복사 생성이나 이동 생성이 일어났을 상황에서도 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;				// float로 변환
....
};

Widget w5(w4); 	// 괄호 사용, 복사 생성자 호출
Widget w6{w4};	// std::initializer_list 생성자 호출(w4가 float로 변환되고 그 float이 long double로 변환
Widget w7(std::move(w4));		// 두번째 생성자 호출
Widget w8{std::move(w4)};		// std::initializer_list 생성자 호출(w6과 마찬가지의 변환이 일어남)

std::initializer_list 생성자가 가능한 최선의 부합인 경우에도 그 생성자를 호출 할 수 없는 현상이 생기기도 한다.

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

Widget w1{10, 5.0};		// 오류, 좁히기 변환이 필요함

컴파일러는 처음 두 생성자를 무시하고 std::initializer_list<bool>을 받는 생성자를 호출하려 한다. 그런데 그 생성자를 호출하려면 int(10)과 double(5.0)을 각각 bool로 변환해야 한다. 두 변환 모두 좁히기 변환인데, 중괄호 초기치는 좁히기 변환이 허용되지 않기 때문에 컴파일러는 코드의 컴파일을 거부한다.

컴파일러가 보통의 중복적재 해소로 물러나는 경우는 중괄호 초기치의 인수 형식들을 std::initializer_list 안의 형식으로 반환하는 방법이 아얘 없을 때 뿐이다.

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 w2{10, true};		// 첫 생성자 호출
Widget w3(10, 5.0);		// 두번째 생성자 호출
Widget w4{10, 5.0};		// 두번째 생성자 호출

int와 bool을 std::string으로 변환하는 방법이 없으므로 비std::initializer_list 생성자들이 다시 중복적재 해소의 후보가 된다.


기본 생성을 지원하며 std::initializer_list 생성도 지원하는 객체를 빈 중괄호 쌍으로 생성한다고 하자. 표준에 따르면 기본 생성자가 호출된다. 빈 중괄호 쌍은 빈 std::initializer_list가 아니라 인수 없음을 뜻한다.

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

Widget w1; 		// 기본 생성자 호출
Widget w2{};		// 기본 생성자 호출
Widget w3();		// 함수 선언!

Widget w4({});		// std::initializer_list 생성자를 빈 초기치 목록으로 호출
Widget w5{{}};		// 마찬가지

중괄호 초기화, std::initializer_list, 생성자 중복적재에 대한 규칙들에 가장 큰 영향을 받는 클래스 중 하나가 std::vector이다. std::vector에는 컨테이너의 초기 크기와 컨테이너의 모든 초기 요소의 값을 지정할 수 있는 비std::initializer_list 생성자가 있다. 그리고 컨테이너의 초기 값들을 지정할 수 있는, std::initializer_list를 받는 생성자도 있다.

std::vector<int> v1(10, 20);	// 비 std::initializer_list 생성자 사용, 모든 요소의 값이 20이고,
				// 요소 10개짜리 std::vector 생성
                
std::vector<int> v2{10, 20};	// std::initializer_list 생성자를 사용, 값이 각각 10, 20인
				// 두 요소를 담은 std::vector 생성

꼭 배워야 할 사항으로 첫째는 클래스 작성할 때, 만일 중복 적재된 생성자 중에 std::initializer_list를 받는 함수가 하나 이상 존재한다면, 중괄호 초기화 구문을 이용하는 클라이언트 코드에는 std::initializer_list 중복적재들만 적용될 수 있음을 주의해야 한다.

두번째로는 클래스 사용자로서 객체를 생성할 때 괄호와 중괄호를 세심하게 선택해야 한다는 것이다.


Conclusion

- 중괄호 초기화는 가장 광범위하게 적용할 수 있는 초기화 구문이며, 좁히기 변환을 방지하며, C++의 가장 성가신 구문 해석에서 자유롭다.

- 생성자 중복적재 해소 과정에서 중괄호 초기화는 가능한 한 std::initializer_list 매개변수가 있는 생성자와 부합된다.

- 괄호와 중괄호의 선택이 의미 있는 차이를 만드는 예는 인수 두개로 std::vector<T>를 생성하는 것이다.

- 템플릿 안에서 객체를 생성할 때 괄호를 사용할 것인지 중괄호를 사용할 것인지 선택하기가 어려울 수 있다.


Reference

Effective Modern C++ 항목 7: 객체 생성 시 괄호()와 중괄호{}를 구분하라

반응형

댓글