int x(0);
int y = 0;
int z{0};
C++11부터 객체 생성 구문이 다양해졌다.
int z = {0};
이런 것도 있는데 이번 항목에서는 다루지 않는다. 대체로 그냥 중괄호만 쓴것과 동일하게 취급된다.
중괄호 초기화는 컨테이너, 비정적 멤버, 복사 불가능한 객체(예:atomic)에 모두 사용이 가능하다. 나머지 두 종류는 경우에 따라 사용가능 여부가 달라진다. 그래서 중괄호 초기화를 균일 초기화라고도 부른다.
double x, y, z;
...
int sum1{x + y + z};
중괄호 초기화의 기능 중 하나는 암묵적인 타입 변환을 방지한다는 것이다.
만약 중괄호 초기화가 아니라면, x + y + z는 계산 결과가 암시적 변환을 통해 int로 초기화 된다.
Widget w1(10); // 생성자 호출
Widget w2(); // 기본 생성자 호출.. 을 하려고 했지만 w2라는 함수를 선언하게 된다
Widget w3{}; // 기본 생성자 호출
또 하나의 기능은 성가신 구문 해석(most vexing parse)에서 자유로워진다.
w2라는 객체의 기본 생성자를 호출하려고 했지만, 선언으로 해석될 수 있기때문에 컴파일러는 선언으로 해석해버린다. 하지만 중괄호 초기화는 의도한대로 기본 생성자를 호출하게된다.
하지만 auto와 다르게 중괄호 초기화를 선호하는것은 안된다. 반드시 상황에 맞춰서 적절하게 사용해야 한다.
몇 가지 예시를 살펴보자.
class Widget {
public:
Widget(int i, bool b); // 1
Widget(int i, double b); // 2
};
Widget w1(10, true); // 1
Widget w2{10, true}; // 1
Widget w3(10, 5.0); // 2
Widget w4{10, 5.0}; // 2
초기화의 종류에 상관없이 생성자 두가지 중 한가지에 대응된다.
class Widget {
public:
...
Widget(std::initializer_list<long double> il); // 3
};
Widget w1(10, true); // 1
Widget w2{10, true}; // 3. long double로 형변환이 강제로 일어남
Widget w3(10, 5.0); // 2
Widget w4{10, 5.0}; // 3. long double로 형변환이 강제로 일어남
그런데 생성자 중에서 하나 이상이 초기화 리스트 타입의 매개변수를 선언한다면 중괄호 초기화 구문은 초기화 리스트를 매개변수로 받는 생성자를 강하게 선호한다.
그냥 선호도 아니고 왜 강하게 선호하냐면, 초기화 리스트를 받는 생성자 호출로 해석될 여지가 아주 조금이라도 있으면 컴파일러는 그 해석을 반드시 선택하기 때문이다.
심지어 이미 존재하는 생성자들(1, 2)보다 더 부합되지 않는 매개변수 타입이더라도 말이다.
class Widget {
public:
...
operator float() const; // float로 변환
};
Widget w5(w4); // 복사 생성자 호출
Widget w6{w4}; // 초기화 리스트 생성자 호출. float로 변환되고 그게 다시 long double로 변환됨
Widget w7(std::move(w4)); // 이동 생성자 호출
Widget w8{std::move(w4)}; // 초기화 리스트 생성자 호출. float로 변환되고 그게 다시 long double로 변환됨
강하게 선호하다보니 복사 생성이나 이동 생성이 일어나는 경우에도 초기화 리스트 생성자가 끼어들게 된다.
class Widget {
public:
...
Widget(std::initializer_list<bool> il); // long double -> bool
};
Widget w{10, 5.0};
long double일때는 더 큰 타입이라서 암시적 변환이 이뤄지기때문에 허용됐지만 이번에는 더 좁은 타입(int, double -> bool)으로 변환되어야 하기 때문에 컴파일러가 거부를 한다.
이미 매개변수의 타입이 완벽하게 부합하는 생성자가 존재함에도 불구하고 중괄호 초기화를 썼기때문에 초기화 리스트 생성자를 호출하려고 시도하는 것이다.
초기화 리스트 생성자가 존재하고 중괄호 초기화를 했음에도 다른 버전의 생성자를 호출하는 경우가 있긴 있다.
bool<->string처럼 암시적 변환 자체가 불가능해서 아예 방법이 존재하지 않을때만 다른 버전의 생성자를 호출한다.
int, double은 bool로 타입 변환시 비트가 깎여서 본래의 값을 유지하지 못할 경우가 훨씬 많지만 어쨌든 '가능'하기 때문에 상황이 달랐던것이다.
class Widget {
public:
Widget();
Widget(std::initializer_list<int> il);
};
Widget w1{}; // 이런 경우는?
Widget w2{{}};
Widget w3({});
만약 생성자가 위와 같이 단 두개만 존재하고 인수가 없는 생성자를 호출한다고 하면 과연 어떤 생성자가 호출될까?
이 경우는 비어있는 초기화 리스트라고 판단하지 않고 인수 자체가 없다고 판단하여 기본 생성자를 호출하게 된다.
혹시라도 초기화 리스트 생성자를 호출하고 싶다면 w2, w3처럼 사용시 비어있는 초기화 리스트라고 판단해서 초기화 리스트 생성자를 호출한다.
중괄호 초기화와 생성자 오버로딩에 따라 결과가 아예 천차만별로 나오는 사례를 하나 보도록 하자.
std::vector<int> v1(10, 20); // size:10, 값:모두20
std::vector<int> v2{10, 20}; // size:2, 값:10, 20
사용되는 생성자가 서로 달라서 결과가 아예 다르기때문에 주의해야한다.
마지막으로 생성자 오버로딩과 초기화 리스트에 관련되어 알아야 할 사항 두가지를 언급한다.
첫번째, 클래스를 작성할 때 초기화 리스트를 매개변수로 받는 생성자가 하나 이상 존재하게 한다면, 중괄호 초기화 구문 사용시 해당 생성자만 적용될 수 있음을 반드시 알아야 한다. 초기화 리스트 생성자는 다른 생성자들과 경쟁하는 수준이 아니라 아예 제외될 정도로 가려버리기 때문이다.
두번째, 객체 생성시 괄호와 중괄호를 신중하게 선택해야 한다. 괄호나 중괄호만 할 수 있는 기능을 파악해두면 선택하는데에 있어서 어려움은 없을 것이다.
또 한가지, 템플릿 작성시에도 고려해야 한다.
◾ 중괄호 초기화는 가장 광범위하게 적용할 수 있는 초기화 구문이며, 좁히기 변환을 방지하며, C++의 가장 성가신 구문 해석에서 자유롭다.
◾ 생성자 오버로딩 해소 과정에서 중괄호 초기화는 가능한 한 std::initializer_list 매개변수가 있는 생성자와 부합한다(심지어 겉으로 보기에 그보다 인수들에 더 잘 부합하는 생성자들이 있어도).
◾ 괄호와 중괄호의 선택이 의미 있는 차이를 만드는 예는 인수 두 개로 std::vector<타입>을 생성하는 것이다.
◾ 템플릿 안에서 객체를 생성할 때 괄호를 사용할 것인지 중괄호를 사용할 것인지 선택하기가 어려울 수 있다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[3장] 항목 9 : typedef보다 별칭 선언을 선호하라 (0) | 2022.12.28 |
---|---|
[3장] 항목 8 : NULL보다 nullptr을 선호하라 (0) | 2022.12.28 |
[2장] 항목 6 : auto가 원치 않은 타입으로 추론될 때에는 명시적 타입의 초기값을 사용하라 (0) | 2022.12.26 |
[2장] 항목 5 : 명시적 타입 선언보다는 auto를 선호하라 (0) | 2022.12.24 |
[1장] 항목 4 : 추론된 타입을 파악하는 방법을 알아두라 (0) | 2022.12.24 |