KoreanFoodie's Study

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

Tutorials/C++ : Advanced

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

GoldGiver 2022. 10. 26. 09:51

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

1. 중괄호 초기화는 가장 광범위하게 적용할 수 있는 초기화 구문이며, 좁히기 변환을 방지하며, C++ 의 가장 성가신 구문 해석에서 자유롭다.
2. 생성자 오버로딩 해소 과정에서 중괄호 초기화는 가능한 한 std::initializer_list 매개변수가 있는 생성자와 부합한다.
3. 괄호와 중괄호의 선택이 의미 있는 차이를 만드는 예는 인수 두 개로 std::vector<수치 형식> 을 생성하는 것이다.
4. 템플릿 안에서 객체를 생성할 때 괄호를 사용할 것인지 중괄호를 사용할 것인지 선택하기가 어려울 수 있다.

 

C++ 11 은 균일 초기화(uniform initialization) 이라는 개념을 도입했는데, 이 때 중괄호를 사용한다. 이는 어디에서나 쓸 수 있는 초기화 방식이다. 예시를 보자.

class Widget
{
private:
	int x{0};
	int y = 0;
	// error: expected identifier before numeric constant
	int z(0);
};

int main()
{
	std::atomic<int> ai1{0};
	std::atomic<int> ai2(0);
	// error: use of deleted function
	std::atomic<int> ai3 = 0;
}

또한 중괄호 초기화는 암묵적 좁히기 변환(narrowing conversion) 을 방지해 준다.

double x, y, z;
// error: 데이터 손실 가능성! 
int sum1{x + y + z};

 

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

// 인수 10으로 Widget 의 생성자 호출
Widget w1(10);

// Widget 을 리턴하는 w2 함수 선언
Widget w2();

// 인수 없이 Widget 의 생성자 호출
Widget w3{};

 

생성자 호출에서 std::initializer_list 매개변수가 관여하지 않는 한 괄호와 중괄호의 의미는 같다. 하지만 중괄호 초기치가 쓰인 호출을 std::initializer_list 를 받는 버전의 생성자 호출로 해석할 여지가 조금이라도 있으면, 컴파일러는 반드시 그 해석을 선택한다. 심지어 복사 생성이나 이동 생성의 경우에도 마찬가지이다. 예시를 보자.

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

	operator float() const { return 0.0; }
};


int main()
{
	// 첫 번째 생성자 호출
	Widget w1(10, true);

	// 세 번째 생성자 호출 (10 과 true 가 long double 로 변환)
	Widget w2{10, true};

	// 두 번째 생성자 호출
	Widget w3(10, 5.0);

	// 세 번째 생성자 호출 (10 과 0.5 가 long double 로 변환)
	Widget w4{10, 5.0};

	// 복사 생성자 호출
	Widget w5(w4);

	// 세 번째 생성자 호출 (w4 가 float 으로 변환 후 long double 로 변환)
	Widget w6{w4};

	// 이동 생성자 호출
	Widget w7(std::move(w4));

	// w6 와 마찬가지의 변환이 일어남
	Widget w8{std::move(w4)};
}

 

이런 특징 때문에, std::initializer_list 가 최선의 부합인 경우에도 해당 생성자를 호출할 수 없는 기이한 상황이 생기기도 한다.

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


int main()
{
	// 에러! 좁히기 변환이 필요함
	Widget w1{10, 5.0};
}

만약 중괄호 초기치의 인수 형식을 std::initializer_list 로 변환하는 방법이 아예 없는 경우에는 비 std::initializer_list 생성자들이 호출된다.

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


int main()
{
	// 첫번째 생성자 호출 (std::string 으로 변환 불가능)
	Widget w1{10, true};
    
    // 두번째 생성자 호출 (std::string 으로 변환 불가능)
    Widget w2{10, 5.0};
}

 

빈 중괄호 쌍은 어떻게 해석될까? 표준에 따르면 기본 생성자가 호출된다. 즉, 빈 std::initializer_list 가 아닌 인수가 없는 생성자를 뜻한다. 만약 인자가 없는 버전의 std::initializer_list 를 호출하고 싶으면, 괄호를 추가로 붙여주면 된다.

class Widget 
{
public:
	Widget() {}
	Widget(std::initializer_list<long double> il) {}
};


int main()
{
	// 기본 생성자 호출
	Widget w1;

	// 기본 생성자 호출
	Widget w2{};

	// 주의! 함수 선언임!
	Widget w3();

	// std::initializer 생성자를 빈 초기치 목록으로 호출
	Widget w4({});

	// std::initializer 생성자를 빈 초기치 목록으로 호출
	Widget w5{{}};
}

 

중괄호 초기화와 괄호 초기화 구분이 필요한 대표적 예시로는 vector<T> 가 있다.

// 각 요소가 20 인 길이 10 짜리 벡터
std::vector<int> v1(10, 20);

// 10, 20 으로 이루어진 길이 2 짜리 벡터
std::vector<int> v2{10, 20};

 

Comments