KoreanFoodie's Study
Effective Modern C++ | 항목 7 : 객체 생성 시 괄호'( )' 와 중괄호'{ }' 를 구분하라 본문
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};
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective Modern C++ | 항목 9 : typedef 보다 별칭 선언을 선호하라 (0) | 2022.10.26 |
---|---|
Effective Modern C++ | 항목 8 : 0 과 NULL 보다 nullptr 를 선호하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 6 : auto 가 원치 않은 형식으로 연역될 때는 명시적 형식의 초기치를 사용하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 5 : 명시적 형식 선언보다는 auto 를 선호하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 4 : 연역된 형식을 파악하는 방법을 알아두라 (0) | 2022.10.26 |