일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- reference
- UE4
- exception
- 오블완
- virtual function
- 암시적 변환
- more effective c++
- lua
- 참조자
- 예외
- 함수 객체
- 반복자
- 스마트 포인터
- effective modern c++
- operator new
- c++
- 언리얼
- 게임
- resource management class
- 티스토리챌린지
- 다형성
- 메타테이블
- effective stl
- Effective c++
- 비교 함수 객체
- 영화 리뷰
- 영화
- implicit conversion
- Smart Pointer
- 상속
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 7. 괄호 초기화 vs 중괄호 초기화 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 7. 괄호 초기화 vs 중괄호 초기화
김디트 2025. 2. 13. 11:52728x90
항목 7. 객체 생성 시 괄호와 중괄호를 구분하라
C++11에서의 객체 생성 구문
int x(0); // 괄호로 초기화
int y = 0; // 등호로 초기화
int z{ 0 }; // 중괄호로 초기화
int z = { 0 }; // 등호와 중괄호로 초기화
// 대체로 중괄호만 사용한 구문과 동일하게 취급된다.
초기화와 할당
- 등호를 사용하면 반드시 할당(assignment)이 일어난다는 것은 오해이다.
- 초기화와 할당의 차이
- int 같은 내장 타입에서는 차이가 없다.
- 사용자 정의 타입에서는 각자 다른 함수를 호출한다.(생성자, operator=)
Widget w1; // 기본 생성자를 호출
Widget w2 = w1; // 복사 생성자를 호출
w1 = w2; // 할당; 복사 할당 연산자(operator=)를 호출
유니폼 초기화(uniform initialization)
- C++98에서는 서로 다른 임의의 값들을 담는 STL 컨테이너를 한 문장으로 생성하는 것이 불가능했다.
- 하지만 모던 C++에서는 유니폼 초기화를 사용하여 해결이 가능하다.
- 중괄호를 사용하므로, 중괄호 초기화(braced initialization)이라고도 한다.
std::vector<int> v{ 1, 3, 5 }; // 서로 다른 임의의 값들로 초기화
- 비정적(non-static) 멤버
- 중괄호 초기화가 가능하다.
class Widget
{
...
private:
int x{ 0 }; // 유니폼 초기화 가능.
int y = 0; // 여전히 가능한 방법.
int z(0); // 에러!
};
- 복사할 수 없는 객체(이를테면 std::atomic(항목 40 참조))
- 중괄호나 괄호로는 초기화가 가능하지만,
- 등호로는 불가능하다.
std::atomic<int> ai1{ 0 }; // 성공
std::atomic<int> ai2(0); // 성공
std::atomic<int> ai3 = 0; // 에러!
- 즉, 유니폼(균일) 초기화라고 불리는 이유는 C++이 지원하는 세 가지 초기화 표현식 어디에서든 모두 사용할 수 있기 때문이다.
- 또 다른 장점으로는, 암묵적 축소 변환(narrowing conversion)을 방지해준다.
- 중괄호 초기화 값이 대상 타입으로 온전히 표현되지 않는 상황이라면 컴파일러가 반드시 알려준다.
double x, y, z;
...
int sum1{ x + y + z }; // double의 합이 int로 표현하지 못할 수 있으므로 에러!
int sum2(x + y + z); // 암묵적 변환 성공
int sum3 = x + y + z; // 여전히 성공
- 또 다른 장점, 가장 성가신 파싱(most vexing parse)에서 자유롭다.
- 선언으로 해석할 수 있는 것은 항상 선언으로 해석해야 한다는 C++ 규칙의 사이드 이펙트이다.
Widget w1(10); // 10으로 Widget의 생성자를 호출
Widget w2(); // 인수 없는 생성자를 호출하고 싶었지만, 함수 선언으로 해석해버린다.(성가신 파싱)
Widget w3{}; // 인수 없는 생성자를 정상적으로 호출한다.
유니폼 초기화의 단점
- 종종 예상치 못한 행동을 보인다.
- 중괄호는 auto에서 std::initializer_list로 추론된다.
- 그와 같은 문제가 중괄호 초기화에서도 발생한다.
class Widget
{
public:
// 생성자에 std::initializer_list 매개변수는 없다.
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 }; // 두 번째 생성자
// 근데 만일 std::initializer_list를 받는 생성자가 추가된다면?
class Widget
{
public:
Widget(int i , bool b);
Widget(int i , double d);
Widget(std::initializer_list<long double> il);
...
};
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로 변환됨)
// 복사 생성이나 이동 생성에도 문제가 생길 수 있다.
class Widget
{
public:
Widget(int i , bool b);
Widget(int i , double d);
Widget(std::initializer_list<long double> il);
...
operator float() const; // float로 변환
};
Widget w5(w4); // 복사 생성자 호출
Widget w6{ w4 }; // std::initializer_list 생성자 호출(Widget->float->long double 변환)
Widget w7(std::move(w4)); // 이동 생성자 호출
Widget w8{ std::move(w4) }; // std::initializer_list 생성자 호출(Widget->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);
...
};
Widget w{ 10, 5.0 }; // 에러! 축소 변환이 필요하다.
- 그 외의 오버로드 함수를 탐색하는 일은, 중괄호 초기화 인수 타입들을 std::initializer_list 안의 타입으로 변환할 방법이 아예 없을 때 뿐이다.
- 즉, 축소 변환까지 불가능한 상황이라야 겨우 탐색을 시작한다.
class Widget
{
public:
Widget(int i , bool b);
Widget(int i , double d);
Widget(std::initializer_list<std::string> il); // std::string을 받는다.
...
};
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();
Widget(std::initializer_list<int> il);
...
};
Widget w1; // 기본 생성자 호출
Widget w2{}; // 여전히 기본 생성자 호출
Widget w3(); // 가장 성가신 파싱! 함수 선언으로 해석된다.
- 빈 std::initializer_list로 std::initializer_list 생성자를 호출하고 싶다면?
- 빈 중괄호를 괄호로 감싸거나
- 빈 중괄호를 또 다른 중괄호로 감싸면 된다.
Widget w4({});
Widget w5{ {} };
템플릿 작성 시의 문제
- 괄호와 중괄호 중 어느 것을 사용해야 하는지 판단하는 게 불가능하다.
- 예를 들어 임의의 개수의 인수로 임의의 타입의 객체를 생성하게 되는 가변 인수 템플릿을 설계 중이라면..
template<typename T, typename... Ts>
void doSomeWork(Ts&&... params)
{
// 버전 1.
T LocalObject(std::forward<Ts>(params)...); // 괄호를 사용한 버전
// 버전 2.
T LocalObject{ std::forward<Ts>(params)... }; // 중괄호를 사용한 버전
...
}
std::vector<int> v;
...
doSomeWork<std::vector<int>>(10, 20); // 이렇게 호출을 했다면..
- 괄호 버전이라면, 요소가 10개인 std::vector이다.
- { 20, 20, 20, 20, 20, 20, 20, 20, 20, 20 }
- 중괄호 버전이라면, 요소가 2개인 std::vector이다.
- { 10, 20 }
- 이 문제는 표준 라이브러리 std::make_unique와 std::make_shared가 해결해야 했던 문제와 완전 동일하다.(항목 21 참조)
- 내부적으로는 괄호를 사용하고, 문서화하여 해결했다.
정리
- 클래스를 작성할 때, 오버로드 생성자 중에 std::initializer_list 버전이 하나라도 있다면
- 중괄호 초기화 구문을 이용하면 std::initializer_list 버전만 적용될 수 있다!
- 그러니 생성자를 설계할 땐 괄호냐 중괄호냐에 따라 다른 오버로드 버전이 선택되지 않도록 설계하자.
- 예컨대, std::initializer_list 버전 생성자가 없던 클래스에 이를 추가하면, 기존 중괄호 구문들이 문제를 일으킬 수 있다.
- 클래스 사용자로서 객체를 생성할 때 괄호와 중괄호를 세심하게 선택해야 한다.
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 8. nullptr (0) | 2025.02.14 |
---|---|
[Effective Modern C++] 6. auto의 타입 추론 실패 (0) | 2025.02.12 |
[Effective Modern C++] 5. 타입 명시보다 auto (0) | 2025.02.11 |
[Effective Modern C++] 4. 추론 타입 파악하기 (0) | 2025.02.10 |
[Effective Modern C++] 3. decltype (0) | 2025.02.07 |
Comments