스토리텔링 개발자

[More Effective C++] 5. 암시적 변환 지양하기 본문

개발/More Effective C++

[More Effective C++] 5. 암시적 변환 지양하기

김디트 2024. 8. 5. 11:12
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를 대신하는 클래스이다.

 

 

 

결론
  • 컴파일러가 맘대로 암시적 타입 변환을 하게 되면 득보다는 실이 많다.
  • 타입 변환 연산자(함수)는 마음 깊은 곳에서 원하지 않는 이상 제공하지 말도록 하자!

 

 

 

참조
 

[Effective C++] 24. 비멤버 함수 구현으로 암시적 변환 지원

항목 24. 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자   클래스에서 암시적 타입 변환을 지원하는 것은 일반적으로 안 좋은 생각이다.다만, 숫자 타입은 C++

delightlane.tistory.com

 

728x90
Comments