본문 바로가기

컴퓨터과학

[Effective C++] (7-1) 객체 생성 시 괄호(())와 중괄호({}) 구분하기

현대적 C++에 적응하기

C++11과 C++14에는 자랑할 만한 '거물급' 기능들이 많이 있습니다. auto, 똑똑한 포인터, 이동 의미론, 람다, 동시성 지원 라이브러리가 그것인데, 이들이 너무나도 중요하기 때문에 각 Chapter에서 모두 다룰 것입니다.

 

* 객체 생성 시 괄호(())와 중괄호({})를 잘 구분해야 합니다.

C++11에서는 객체 생성 구문이 아주 다양해졌습니다. 이를 두고 선택의 폭이 넓어져서 좋다고 볼 수도 있지만, 불필요한 혼동의 근원으로 볼 수도 있을 것입니다. 일반적인 규칙을 말하자면, C++11에서는 초기화 값을 괄호로 지정할 수도 있고, 등호로 지정할 수도 있고, 중괄호로 지정할 수도 있습니다.

 

<의사 코드>

int x(0); // 초기치를 괄호로 감싼 예

int y = 0; // 초기치를 "=" 다음에 지정한 예

int z{ 0 }; // 초기치를 중괄호로 감싼 예

 

그리고 다음처럼 등호와 중괄호를 함께 사용할 수 있는 경우도 많습니다.

 

int z = { 0 }; // "="와 중괄호로 초기치를 지정한 예

 

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

다양한 초기화 구문들을 불필요한 혼동의 근원으로 간주하는 사람들의 한 가지 주장은, C++ 초보자는 등호를 이용한 초기화 구문을 보고 배정(assignment)이 일어난다고 오해하는(실제로는 배정이 일어나지 않는다) 경우가 많다는 것입니다. int 같은 내장 형식에서 초기화와 배정은 그냥 학술적인 차원에서만 차이가 나지만, 사용자 정의 형식에서는 초기화와 배정이 각자 다른 함수들을 호출하므로 둘을 구분하는 것이 중요합니다.

 

Widget w1; // 기본 생성자를 호출

Widget w2 = w1; // 배정이 아님; 복사 생성자를 호출

w1 = w2; // 배정; 복사 배정 연산자(operator=)를 호출

 

C++98도 여러 가지 초기화 구문을 지원했지만, 원하는 초기화를 명시적으로 표현할 수 없는 상황이 있었습니다. 예를 들어 서로 다른 임의의 값들(이를테면 1, 3, 5 등)을 담는 STL 컨테이너를 직접 생성하는 것이 불가능했기 때문에, 일단 컨테이너를 생성한 후 값들을 추가 해야 했습니다. 

여러 가지 초기화 구문이 주는 혼동을 완화하기 위해, 그릭리고 그 구문들이 모든 가능한 초기화 시나리오를 포괄하지는 는 않는다는 사실을 해결하기 위해, C++11은 균일 초기화(uniform initialization)를 도입했습니다. 이것은 어디서나 사용할 수 있고 모든 것을 표현할 수 있는(적어도 개념상으로는) 단 한 종류의 초기화 구문입니다. 균일 초기화 구문은 중괄호를 사용하며, 그래서 이 책에서는 균일 초기화보다 중호로 감싼 초기화(braced initialization), 줄여서 중괄호 초기화라는 표현을 선호합니다. '균일 초기화'는 하나의 개념이지만, '중괄호 초기화'는 구문적인 구성체입니다.

중괄호 초기화를 이용하면 이전에는 표현할 수 없었던 방식의 객체 생성을 표현할 수 있습니다. 컨테이너의 초기 내용을 중괄호를 이용해서 지정하는 것은 다음과 같이 아주 간단합니다.

 

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

 

중괄호 구문은 비정적(non-static) 자료 멤버의 기본 초화 값을 지정하는 데에도 사용할 수 있습니다. C++에서 새로 생긴 이러한 능력은 괄호 없이 "="만 있는 초기화 구문으로도 사용할 수 있습니다.

 

class Widget

{

 ...

private:

 int x{ 0 }; // OK, x의 기본값은 0

 int y = 0; // 역시 OK

 int z(0); // 오류!

};

 

반면, 복사할 수 없는 객체(이를테면 std::atomic - Chapter(40) 참고)는 중괄호나 괄호로는 초기화할 수 있지만 "="로는 초기화할 수 없습니다.

 

std::atomic<int> ail{ 0 }; // OK

std::atomic<int> ai2(0); // OK

std::atomic<int> ai3 = 0; // 오류!

 

이상의 예를 보면 중괄호 초기화를 '균일' 초기화라고 부르는 이유를 쉽게 이해할 수 있을 것입니다. C++이 지원하는 세 가지 초기화 표현식 지정 방법 중 어디서나 사용할 수 있는 것은 중괄호 구문뿐입니다.

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

 

double x, y, z;

...

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

 

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

 

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

int sum3 = x + y + z; // 마찬가지

 

중괄호 초기화의 또 다른 주목할 만한 특징은, C++에서 가장 성가신 구문 해석(most vexing parse)에 자유롭다는 점입니다. 가장 성가신 구문 해석은 "선언으로 해석할 수 있는 것은 항상 선언으로 해석해야 한다"는 C++의 규칙에서 비롯된 하나의 부작용인데, 혹시 독자가 그냥 기본 생성자를 이용해서 객체를 생성하려고 했지만 의도와는 달리 함수를 선언하게 된 경험이 있다면 바로 이 부작용에 당한 것입니다. 문제의 근원은 이렇습니다. 다음은 흔히 인수를 지정해서 생성자를 호출하는 코드의 예입니다.

 

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

 

그런데 이와 거의 비슷한 구문을 이용해서 인수 없이 Widget의 생성자를 호출하려 하면, 사실은 객체가 아니라 함수를 선언하게 됩니다.

 

Widget w2(); // 가장 성가신 구문 해석! Widget을 돌려주는, w2라는 이름의 함수를 선언합니다

 

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

 

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