728x90
반응형

중괄호 초기화(균일화 초기화)

변수 초기화 방법은 기존의 초기화 방법에, 중괄호 초기화 방법을 더해 총 4가지 방법으로 변수를 초기화 할 수 있다.

int a(0); // 초기치를 괄호로 감싼 예
int b = 0; // 초기치를 "=" 다음에 지정한 예
int c{ 0 }; // 초기치를 중괄호로 감싼 예
int d = { 0 }; // "="와 중괄호로 초기치를 지정한 예

이번 항목의 나머지 부분에서는 이러한 등호와 중괄호 구문은 무시한다. 대체로 C++은 이를 중괄호만 사용한 구문과 동일하게 취급하기 때문이다.

중괄호 초기화를 이용하면 이전에는 표현할 수 없었던 방식의 객체 생성을 표현할 수 있다.

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

 

중괄호 구문은 비정적 데이터 멤버의 기본 초기화 값을 지정하는 데에도 사용할 수 있다.
즉, 괄호 없이 "="만 있는 초기화 구문으로도 사용할 수 있다.

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

반면에 복사할 수 없는 객체(ex. std::atomic)는 중괄호나 괄호로는 초기화 할 수 있지만, "="로는 초기화 할  수 없다.

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

중괄호 초기화의 혁신적인 기능 하나는 내장 기능들 사이의 암묵적 축소 변환을 방지해 준다는 것이다.

아래의 예시를 봐보자.

double x, y, z;
...
int sum1{ x + y + z }; // 오류! double들의 합을 int로 표현하지 못할 수 있음.

// 괄호나 "="를 이용한 초기화는 이러한 축소 변환을 점검하지 않는다.
// 그런 점검을 강제하면 기존의 코드가 더 이상 컴파일되지 않는 경우가 너무 많아지기 때문이다.

int sum2(x + y + z); // OK(표현식의 값이 int에 맞게 잘려나감)
int sum3 = x + y + z; // OK

또 다른 특징은, 구문 해석에 자유롭다는 점이다.

구문 해석은 "선언으로 해석 할 수 있는 것은 항상 선언으로 해석해야 한다."는 C++의 규칙에서 비롯된 부작용이다.

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

생성자 호출에서 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를 받는 오버로딩 버전을 강하게 선호한다.

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

// 이렇게 하면, 더 나쁜 선택임에도 Widget의 인스턴스 w2와 w4는 새 생성자를 통해서 생성된다.
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(void) 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 생성자를 호출 할 수 없는 경우에도 std::initializer_list 생성자를 우선하며, 컴파일러가 오버로딩 해소로 물러나는 경우는 중괄호 초기치의 인수 타입을 절대 변환할 수 없을 때 뿐이다.

class Widget
{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    
    Widget(std::initializer_list<bool> il); // 요소의 타입이 bool

    ... // 암묵적 변환 함수 없음
};

Widget w{ 10, 5.0 }; // 오류! 축소 변환이 필요함

---
// 절대 변환할 수 없는 경우
class Widget
{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    
    Widget(std::initializer_list<std::string> il); // 요소의 타입이 string
    ... // 암묵적 변환 함수 없음
};

Widget w1(10, true); // 괄호 사용, 첫 생성자를 호출
Widget w2{ 10, true }; // 중괄호 사용, 첫 생성자 호출
Widget w3(10, 5.0); // 괄호 사용, 둘째 생성자를 호출
Widget w4{ 10, 5.0 }; // 중괄호 사용, 둘째 생성자 호출

하지만 여기서 빈 중괄호 쌍은 기본 생성자가 호출 되는데 즉, 빈 중괄호 쌍은 빈 std::initializer_list가 아니라 인수 없음을 뜻하기 때문이다.

class Widget
{
public:
    Widget(void); // 기본 생성자
    Widget(std::initializer_list<int> il); // std::initializer_list 생성자

    ... // 암묵적 변환 함수 없음
};

Widget w1; // 기본 생성자를 호출
Widget w2{}; // 기본 생성자를 호출
Widget w3(); // 가장 성가신 구문 해석! 함수 선언!

// 빈 std::initializer_list로 std::initializer_list 생성자를 호출하고 싶다면, 다음처럼 빈 중괄호 쌍을 괄호로 감싸거나 빈 중괄호 쌍을 또 다른 중괄호 쌍으로 감싸면 된다.
Widget w4({}) // std::initializer_list 생성자를 빈 초기치 리스트로 호출
Widget w5{{}} // 마찬가지

이러한 문제들 때문에 대부분의 개발자들은 둘 중 하나를 기본으로 삼아서 사용하고, 다른 하나는 꼭 필요할 때에만 사용한다.

 

std::vector에는 컨테이너의 초기 크기와 컨테이너의 모든 초기 요소의 값을 지정할 수 있는 비 std::initializer_list 생성자가 있다. 그런데, 컨테이너의 초기 값들을 지정할 수 있는 std::initializer_list를 받는 생성자도 있다.

이 때문에 수치 타입의 값들을 담는 std::vector를 생성할 때 생성자에 인수 두 개를 지정한다면, 그 인수들을 괄호로 감싸느냐 중괄호로 감싸느냐에 따라 아주 다른 결과가 나온다.

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가 생성됨

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

예를 들어 임의의 개수 인수들을 지정해서 임의의 타입 객체를 생성한다고 하자.

개념적으로는, 가변 인수 템플릿을 이용하면 만드는 것이 전혀 복잡하지 않다.

하지만 아래의 예시를 봐보자.

template <typename T, typename... Ts> // 생성할 객체의 타입, 사용할 인수들의 타입들
void doSomeWork(Ts&&... params)
{
    params...으로부터 지역 T 객체를 생성한다;
    ...
}

// 의사코드로 표시된 줄을 실제 코드로 바꾸는 방법은 다음 두 가지이다.
T localObject(std::forward<T>(params)...); // 괄호를 사용
T localObject{ std::forward<T>(params)... }; // 중괄호를 사용

// 이제 다음과 같은 호출 코드를 생각해 보자.
std::vector<int> v;
...
doSomeWork<std::vector<int> >(10, 20);

// 만일 doSomeWork가 괄호를 이용해서 localObject를 생성한다면, 그 결과는 요소가 10개인 std::vector이다. doSomeWork가 중괄호를 이용한다면 결과는 요소가 2개인 std::vector이다.
// 어느 쪽이 옳은지는 doSomeWork 작성자가 알 수 없다. 오직 호출자만이 알 수 있을 뿐이다.

이는 표준 라이브러리 함수 std::make_unique와 std::make_shared가 해결해야 했던 문제와 동일하다.

두 함수는 내부적으로 괄호를 사용하고, 그러한 결정을 인터페이스의 일부에 문서화함으로써 이 문제를 해결한다.

728x90
반응형