일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |
Tags
- operator new
- 티스토리챌린지
- lua
- 영화 리뷰
- 다형성
- 루아
- 반복자
- c++
- 참조자
- 게임
- resource management class
- 스마트 포인터
- Effective c++
- implicit conversion
- reference
- more effective c++
- 예외
- 상속
- 암시적 변환
- UE4
- 메타테이블
- 오블완
- exception
- Smart Pointer
- 비교 함수 객체
- 영화
- effective stl
- 함수 객체
- 언리얼
- virtual function
Archives
- Today
- Total
스토리텔링 개발자
[More Effective C++] 5. 암시적 변환 지양하기 본문
728x90
항목 5. 사용자 정의 타입 변환 함수에 대한 주의를 놓지 말자
암시적 변환
- C처럼 C++ 역시 암시적 변환을 지원한다.
- 예컨대 char -> int, short -> double 로 군소리 없이 변환시켜 준다.
- 심지어 int -> short, double -> char 처럼 데이터 손상 여지가 있는 변환도 지원한다.
- raw 타입에 대해선 어쩔 수 없지만, 커스텀 타입에 대해서는 이 암시적 변환을 확실히 제어할 수 있다.
- 암시적 타입 변환을 위해 컴파일러가 사용할 수 있는 함수를 제공하면 된다.
암시적 타입 변환 함수의 종류
1. 단일 인자 생성자(single-argument constructor)
- 인자를 하나만 받아 호출하는 생성자를 말한다.
- 하나의 매개변수만 받도록 선언.
- 혹은 여러 매개변수이나, 처음 것을 제외한 나머지는 기본값을 갖도록 선언.
class Name
{
public:
Name(const string& s); // 인자가 하나인 생성자
...
};
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1); // 기본값을 제공하는 생성자
...
}
2. 암시적 타입변환 연산자 (implicit type conversion operator)
- 함수 앞에 operator가 덜렁 붙어 있는, 이상한 모양의 멤버함수일 뿐이다.
- 이 함수(연산자)는 반환값을 선언할 수 없다.
- 반환값의 타입이 즉 함수의 이름이다.
class Rational
{
public:
...
operator double() const; // double로 암시적 형변환
};
Rational r(1, 2);
double d = 0.5 * r; // r을 double로 형변환 후 곱셈한다.
타입변환 함수를 되도록 제공하지 않아야 하는 이유
- 타입변환 함수들이 원하든 원하지 않든 호출되고 만다.
- 그로 인해 프로그램의 동작이 엉뚱해지고 비직관적으로 변환다.
- 오류를 찾아내기도 끔찍하게 어렵다.
암시적 타입변환 연산자의 원치 않은 호출
Rational r(1, 2);
cout << r; // operator<< 함수를 작성하는 걸 잊었으므로 컴파일 에러가 발생해야 하지만..
// 실제로는 암시적 타입 변환 함수로 인해 double로 변환되며 컴파일이 성공한다.
// 하지만 이는 원하지 않는 동작이다.
- 해결법
- 암시적 타입변환 연산자를, 똑같은 일을 하되 다른 이름을 갖는 함수로 바꿔치기 해야 한다.
class Rational
{
public:
...
double asDouble() const;
};
Rational r(1, 2);
cout << r; // 컴파일 에러!!
cout << r.asDouble(); // 컴파일 성공. 의도가 명확하다.
- 물론 불편하지만, 의도치 않은 잘못된 함수 호출을 막을 수 있다.
- 이런 문제로 인해 C++ 프로그래머들은 타입변환 연산자를 꺼린다.
- string 클래스 역시 char*로의 암시적 변환 연산자 대신 c_str 메소드를 제공한다.
단일 인자 생성자의 원치 않은 호출
template<typename T>
class Array
{
public:
Array(int lowBound, int highBound);
// 단일 인자 생성자
// 사용자가 배열 크기를 정할 수 있도록 인자를 받는다.
// 하지만, 의도와 다르게 타입변환 함수로 쓰일 수도 있다..
Array(int size);
T& operator[](int index);
...
}
bool operator==(const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for(int i = 0 ; i < 10 ; ++i)
{
// a[i] 여야 하지만 오타가 났다!!!
// 하지만 컴파일 에러가 발생하지 않는다..
// Array<int> == int 로 해석하기 때문이다.
// int는 Array<int>로 단일 인자 생성자로 인해 암시적 변환한다.
if(a == b[i])
{
...
}
else
{
...
}
}
- 결국 위의 비교문은 아래처럼 해석된다.
for(int i = 0 ; i < 10 ; ++i)
if(a == static_cast< Array<int> >(b[i])) ...
- 루프마다 b[i]만큼의 크기를 가지는 임시 배열이 생성되며, 이는 무척이나 비효율적이다.(항목 19 참조)
- 해결법
- 역시나 그런 연산자를 선언하지 않으면 된다.
- 하지만 암시적 타입변환 연산자와는 달리 단일 인자 생성자는 그리 쉽게 회피할 수 있는 것이 아니라는 게 문제..
- 단일 인자 연산자는 진짜로 제공해야 할 일이 많기 때문이다.
- 하지만 이를 불가능하게 하는 C++의 기능이 존재하는데...
- explicit 키워드
- 암시적 타입 변환을 막는다.
- 그저 생성자 앞에 이 키워드를 붙여주기만 하면 된다.
template<typename T>
class Array
{
public:
...
explicit Array(int size); // 암시적 변환을 막는다.
...
};
Array<int> a(10);
Array<int> b(10);
if(a == b[i]) ... // 암시적 변환이므로 컴파일 에러!
if(a == Array<int>(b[i])) ... // 명시적 변환이므로 통과.
if(a == static_cast< Array<int> >(b[i])) ... // 여전히 명시적 변환이므로 통과.
if(a == (Array<int>)b[i]) ... // c 스타일 캐스트로 명시적 변환이므로 통과.
- 혹여나 explicit을 지원하지 않는 컴파일러라면(그럴리는 없겠지만..)
- ‘사용자 정의 타입 변환 함수는 두 개 이상 한번에 쓰이지 않는다’ 규칙을 사용한다.
template<typename T>
class Array
{
public:
class ArraySize
{
public:
ArraySize(int numElements) : theSize(numElements) {}
int size() const { return theSize; }
private:
int theSize;
};
Array(int lowBound, int highBound);
Array(ArraySize size); // size를 위한 내부 클래스를 지원하도록 수정
...
}
bool operator==(const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10); // ArraySize(int) 생성자로 인해 암시적 변환이 일어나며 통과.
Array<int> b(10);
...
for(int i = 0 ; i < 10 ; ++i)
{
if(a == b[i]) ... // int를 받는 생성자는 이제 없으므로 컴파일 에러!
// 컴파일러는 int -> ArraySize -> Array<int> 순으로 생성할 정도로 깊게 처리할 수 없다.
// 왜나면 그러기 위해서는 두 번의 사용자 정의 변환을 거쳐야 하기 때문이다.
// 앞에서 언급한 규칙에 위배된다.
}
- ArraySize 처럼 사용되는 클래스를 프록시 클래스라고 한다.
- 프록시 클래스(proxy class) (항목 30 참조)
- 다른 객체를 대신하는 클래스
- 위의 경우 ArraySize는 int를 대신하는 클래스이다.
결론
- 컴파일러가 맘대로 암시적 타입 변환을 하게 되면 득보다는 실이 많다.
- 타입 변환 연산자(함수)는 마음 깊은 곳에서 원하지 않는 이상 제공하지 말도록 하자!
참조
728x90
'개발 > More Effective C++' 카테고리의 다른 글
[More Effective C++] 7. operator&& / operator|| / operator, (0) | 2024.08.07 |
---|---|
[More Effective C++] 6. operator++ / operator-- (0) | 2024.08.06 |
[More Effective C++] 4. 불필요한 기본 생성자 미제공하기 (0) | 2024.08.04 |
[More Effective C++] 3. 다형성 객체의 배열 지양하기 (0) | 2024.08.03 |
[More Effective C++] 2. C++ 스타일 캐스팅 (0) | 2024.08.02 |
Comments