타입 변환(Conversion)

C++에서는 객체와 다른 타입 간의 변환을 위한 다양한 방법을 제공합니다.

변환 연산자

객체가 다른 타입으로 변환될 때 호출되는 함수:

  class Int32 {
    int value;
public:
    Int32() : value(0) {}
    Int32(int n) : value(n) {}
    
    // Int32를 int로 변환하는 연산자
    operator int() const { return value; }
};

int main() {
    int pn;
    Int32 un;
    
    pn = un;  // un.operator int() 호출
    un = pn;  // 아래 두 가지 중 하나로 해석
              // 1. un.operator=(pn) - 대입 연산자
              // 2. un = Int32(pn) - 변환 생성자 + 대입
}
  

타입 변환 초기화 방법

  Int32 n1(3);     // 직접 초기화
Int32 n2 = 3;    // 복사 초기화 -> Int32 n2 = Int32(3)와 동일
Int32 n3{3};     // 유니폼 초기화(C++11)
Int32 n4 = {3};  // 유니폼 초기화 + 복사 초기화
n1 = 3;          // 대입 연산에서의 변환
  

C++14까지의 처리 과정 (Int32 n2 = 3)

  1. 인자 1개인 생성자를 이용해 Int32 임시 객체 생성
  2. 생성된 임시 객체를 복사/move 생성자로 복사
  3. 컴파일러가 최적화하여 임시 객체 생성을 제거하는 경우가 많음

C++17 이후

임시 객체를 생성하지 않고, 인자 1개인 생성자를 직접 호출하는 방식으로 최적화됨

explicit 키워드

생성자나 변환 연산자가 암시적 변환의 용도로 사용되는 것을 방지합니다.

explicit 생성자

  class Vector {
public:
    explicit Vector(int size) {}
};

void foo(Vector v) {}

int main() {
    Vector v1(3);     // 직접 초기화 - 허용
    // Vector v2 = 3; // 복사 초기화 - 오류
    // v1 = 3;        // 대입 - 오류
    // foo(3);        // 함수 인자로 암시적 변환 - 오류
}
  

explicit 변환 연산자

  class Machine {
    bool state;
public:
    // explicit가 없으면 다른 변환 연산자와 충돌 가능
    explicit operator bool() { 
        return state; 
    }
};

Machine m;
if (m) { /* m이 유효한지 검사 */ }

// explicit가 없다면 아래와 같은 잘못된 코드도 컴파일됨
// m << 10;  // bool로 변환 후 비트 시프트 연산 시도
  

C++20: 조건부 explicit

C++20부터는 조건에 따라 explicit 동작을 설정할 수 있습니다:

  template<typename T>
class Wrapper {
public:
    // T가 정수 타입이 아닐 때만 explicit
    explicit(!std::is_integral_v<T>) Wrapper(T value);
};
  

특수 변환 예제

nullptr

  struct nullptr_t {
    template<typename T>
    constexpr operator T*() const { 
        return 0; 
    }
};

nullptr_t null_ptr;
void foo(int* p) {}

int main() {
    foo(null_ptr);  // operator int*() 호출
}
  

반환 타입 추론(Return Type Resolver)

  struct Alloc {
    std::size_t size;
    
    Alloc(std::size_t sz) : size(sz) {}
    
    template<typename T>
    operator T*() {
        return new T[size];
    }
};

int main() {
    int* p1 = Alloc(10);    // operator int*() 호출
    double* p2 = Alloc(10); // operator double*() 호출
}
  

이 예제에서 Alloc 클래스는 템플릿 변환 연산자를 통해 어떤 포인터 타입으로도 변환될 수 있습니다. 변환 과정에서 요청된 타입의 배열을 동적으로 할당하여 반환합니다.

  • int* p1 = Alloc(10);에서는 Alloc(10) 객체가 operator int*() 변환을 통해 int[10] 배열을 할당하고 그 포인터를 반환합니다.
  • double* p2 = Alloc(10);에서는 같은 Alloc(10) 객체가 operator double*() 변환을 통해 double[10] 배열을 할당하고 그 포인터를 반환합니다.

이런 패턴은 사용 컨텍스트에 따라 자동으로 적절한 타입의 메모리를 할당하는 스마트 할당자를 구현할 때 유용합니다. 단, 메모리 누수를 방지하기 위해 실제 사용 시에는 적절한 메모리 관리 전략(스마트 포인터 등)이 필요합니다.