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

게시:     수정

카테고리:

태그: ,

이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역

📦 3. 현대적 C++에 적응하기

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

int x(0); // 소괄호
int y = 0; // 등호

// 아래 두 구문은 동일하게 취급된다.
int z{ 0 }; // 중괄호
int z = { 0 }; // 중괄호 + 등호
  • C++11에서 객체 생성 구문이 다양해졌다.
Widget w1; // 기본 생성자 호출
Widget w2 = w1; // 복사 생성자 호출
w1 = w2; // 복사 대입 생성자 호출
  • 초기화와 대입은 각자 다른 함수를 호출한다.
    • =을 사용하면, 대입이 일어나지 않는다.

📌 균일 초기화 (Uniform Initialization)

  • 여기선 중괄호 초기화라는 표현을 사용한다.
  • vector<int> v{ 1, 3, 5 }; 처럼 사용할 수 있다.

✅ 특징 1: 어디서나 사용 가능

// 비정적 자료 멤버 초기화의 경우
class Widget {
private:
	int x{ 0 }; // ✅
	int y = 0; // ✅
	int z(0); // ⚠️ 오류!
};
// 복사할 수 없는 객체(atomic)의 경우
atomic<int> ai1{ 0 }; // ✅
atomic<int> ai2(0); // ✅
atomic<int> ai3 = 0; // ⚠️ 오류!
  • 복사할 수 없는 객체는 소괄호/중괄호로는 초기화할 수 있지만, =로는 초기화할 수 없다.

✅ 특징 2: 축소 변환(Narrowing Conversion) 방지

double x, y, z;

// ⚠️ 오류!
// double → int로의 축소 변환 불가!
int sum1{ x + y + z };

int sum2( x + y + z ); // ✅ double → int
int sum3 = x + y + z; // ✅ 동일
  • 암묵적 축소 변환을 방지해준다.
  • 소괄호와 등호를 사용한 초기화는 축소 변환을 점검하지 않는다.

✅ 특징 3: 가장 성가신 구문 해석(Most Vexing Parse) 회피

// int를 받는 생성자 호출
Widget w1(10);

// ⚠️ 가장 성가신 구문 해석!
// Widget을 반환하는 w2 함수 선언
Widget w2();

// 인수 없는 생성자 호출
Widget w3{};
  • 가장 성가신 구문 해석: 선언으로 해석할 수 있는 것은 항상 선언으로 해석해야 한다는 부작용이다.
  • 중괄호 초기화는 해당 부작용에서 자유롭다.

⚠️ 특징 4: 오버로딩 우선순위

class Widget {
public:
	Widget(int i, bool b); // ①
	Widget(int i, double d); // ②
};

Widget w1(10, true); // ① 호출
Widget w2{10, true}; // ① 호출

Widget w3(10, 5.0); // ② 호출
Widget w4{10, 5.0}; // ② 호출
  • 평범한 코드이며, 흐름도 예상대로 흘러간다.
class Widget {
public:
	Widget(int i, bool b); // ①
	Widget(int i, double d); // ②
	Widget(std::initializer_list<long double> il); // ③
		
	operator float() const;
};

Widget w1(10, true); // ① 호출
Widget w2{10, true}; // ③ 호출 (10, true가 long double로 변환)

Widget w3(10, 5.0); // ② 호출
Widget w4{10, 5.0}; // ③ 호출 (10, 5.0이 long double로 변환)

Widget w5(w4); // 복사 생성자 호출
Widget w6{w4}; // ③ 호출 (w4 → float → long double 변환)

Widget w7(std::move(w4)); // 이동 생성자 호출
Widget w8{std::move(w4)}; // ③ 호출 (w4 → float → long double 변환)
  • std::initializer_list 형식 매개변수가 있는 생성자가 추가되면, 달라진다.
    • 중괄호 초기화 사용 시, 해당 생성자를 오버로딩 최우선 순위로 잡는다!
  • 복사/이동 생성자가 호출되어야 할 상황에서도, 가로채는 일이 발생한다!
class Widget {
public:
	Widget(int i, bool b); // ①
	Widget(int i, double d); // ②
	Widget(std::initializer_list<bool> il); // ③
};

// ⚠️ 컴파일 오류!
// int, double → bool로의 축소 변환 방지됨!
Widget w{10, 5.0};
  • ② 생성자가 있음에도, ③ 생성자가 채간다.
    • 이로 인해, 오류가 발생할 수 있다.
class Widget {
public:
	Widget(int i, bool b); // ①
	Widget(int i, double d); // ②
	Widget(std::initializer_list<string> il); // ③
};

Widget w1(10, true); // ① 호출
Widget w2{10, true}; // ① 호출

Widget w3(10, 5.0); // ② 호출
Widget w4{10, 5.0}; // ② 호출
  • 아예 변환이 불가능한 경우, 생성자를 가로채는 일이 발생하지 않는다.
class Widget {
public:
	Widget();
	Widget(std::initializer_list<int> il); // ③
};

Widget w1; // 기본 생성자 호출
Widget w2{}; // 기본 생성자 호출
Widget w3(); // ⚠️ 가장 성가신 구문 해석으로 인한 함수 선언
Widget w4({}); // ③ 호출
Widget w5{()}; // ③ 호출
  • 빈 중괄호 쌍은 인수 없음을 뜻하며, 기본 생성자를 호출한다.
  • std::initializer_list로 ③ 생성자를 호출하기 위해: ({}), {()} 형태로 사용한다.

💡 알아두어야 할 점

  • 오버로딩된 생성자 중 std::initializer_list를 받는 함수가 하나 이상 존재하면, 중괄호 초기화 구문을 이용하는 코드에 해당 함수만 적용될 수 있다는 것을 주의하자.
  • 생성자를 설계할 때, 소괄호/중괄호에 따라 다른 오버로딩 버전이 선택되지 않도록 하자.
    • e.g. vector<int> v1(10, 20)vector<int> v2{10, 20}의 결과가 다르며, 이는 설계의 오류이다.
  • 클래스를 사용할 때, 소괄호 혹은 중괄호를 세심하게 선택하자.
    • 둘 중 하나를 선택해, 일관되게 적용하자.
template<typename T, typename... Ts>
void doSomeWork(Ts&&... params) {
	// ① 소괄호 사용하는 경우
	T localObject(std::forward<Ts>(params)...);
	// ② 중괄호 사용하는 경우
	T localObject{std::forward<Ts>(params)...};
	...
}

vector<int> v;
doSomeWork<vector<int>>(10, 20); // localObject: 요소가 10개인 vector
doSomeWork<vector<int>>{10, 20}; // localObject: 요소가 2개인 vector
  • 템플릿 작성자는 소괄호/중괄호 중 하나를 선택한다.
  • 사용자는 소괄호/중괄호 중 무엇을 사용해야 하는지 알 수 없다.
  • 그러므로 인터페이스 일부에 문서화하여 문제를 해결해야 한다.
    • 이는 make_unique, make_shared(항목 21 참고)에서 해결해야 했던 문제와 동일하다.

🧐 정리

  • 중괄호 초기화는:
    • 광범위하게 사용 가능한 초기화 구문
    • 축소 변환 방지
    • 가장 성가신 구문 해석에서 자유로움
  • 생성자 오버로딩 처리 과정에서 중괄호 초기화는 가능한 std::initializer_list 매개변수가 있는 생성자와 부합한다.
  • 소괄호와 중괄호의 선택이 의미 있는 차이를 만드는 예는 인수 두 개로 vector<형식>을 생성하는 것이다.
  • 템플릿 안에서 객체를 생성할 때 소괄호/중괄호 중 무엇을 사용할지 선택하기 어려울 수 있다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기