변환 연산자(Conversion operator, cast operator)

변환 연산자(Conversion operator, cast operator)

어떤 클래스 타입의 객체를 다른 타입으로 자연스럽게 바꿔 주고 싶을 때가 있어요. 그때 클래스 안에 정의하는 것이 **변환 연산자(conversion function, cast operator)**예요. 클래스 타입에서 다른 타입으로의 암시적 변환이나 명시적 변환을 가능하게 해 주는 멤버 함수예요.

출처: cppreference

본문

문법

변환 함수는 매개변수도, 명시적 반환 타입도 없고 다음 형태의 이름을 가진 비정적 멤버 함수(또는 멤버 함수 템플릿)로 선언해요.

operator 변환-타입-id (1)
explicit operator 변환-타입-id (2) (C++11부터)
explicit ( 표현식 ) operator 변환-타입-id (3) (C++20부터)

**변환-타입-id(conversion-type-id)**는 type-id인데, 단 그 선언자에는 함수나 배열 선언자 [] 또는 ()가 허용되지 않아요(그래서 배열에 대한 포인터 같은 타입으로의 변환은 타입 별칭/typedef나 항등 템플릿이 필요해요. 아래 참고). typedef를 쓰더라도 변환-타입-id가 배열이나 함수 타입을 나타낼 수는 없어요. 이 함수의 반환 타입은 변환-타입-id가 나타내요.

선언에 명시적 반환 타입은 허용되지 않지만, 선언 문법의 decl-specifier-seq은 올 수 있고 type-specifier나 키워드 static을 제외한 어떤 지정자도 포함할 수 있어요. 특히 explicit 외에도 inline, virtual, constexpr(C++11부터), consteval(C++20부터), friend 지정자가 허용돼요(friend는 정규화된 이름을 요구해요: friend A::operator B();처럼).

클래스 X 안에 이런 멤버 함수를 선언하면, X에서 변환-타입-id로의 변환을 수행해요.

struct X
{
    // 암시적 변환
    operator int() const { return 7; }
    
    // 명시적 변환
    explicit operator int*() const { return nullptr; }
    
    // Error: 배열 연산자는 변환-타입-id에 허용되지 않음
//  operator int(*)[3]() const { return nullptr; }
    
    using arr_t = int[3];
    operator arr_t*() const { return nullptr; } // typedef를 통하면 OK
//  operator arr_t () const; // Error: 어떤 경우에도 배열로 변환은 허용되지 않음
};

int main()
{
    X x;

    int n = static_cast<int>(x);   // OK: n을 7로 설정
    int m = x;                     // OK: m을 7로 설정

    int* p = static_cast<int*>(x); // OK: p를 null로 설정
//  int* q = x; // Error: 암시적 변환 없음

    int (*pa)[3] = x;  // OK
}

동작 방식(Explanation)

사용자 정의 변환 함수는 암시적 변환의 두 번째 단계에서 호출돼요. 이 단계는 변환 생성자 0개 또는 1개, 사용자 정의 변환 함수 0개 또는 1개로 구성돼요.

어떤 사용자 정의 변환을 수행하는 데 변환 함수와 변환 생성자 모두 사용될 수 있다면, 복사 초기화(copy-initialization)와 참조 초기화(reference-initialization) 문맥에서는 오버로드 결정이 변환 함수와 생성자를 둘 다 고려하지만, 직접 초기화(direct-initialization) 문맥에서는 생성자만 고려돼요.

struct To
{
    To() = default;
    To(const struct From&) {} // 변환 생성자
};

struct From
{
    operator To() const {return To();} // 변환 함수
};

int main()
{
    From f;
    To t1(f);  // 직접 초기화: 생성자 호출
    // 참고: 변환 생성자를 사용할 수 없으면 암시적 복사 생성자가 선택되고,
    // 그 인자를 준비하기 위해 변환 함수가 호출돼요
    
//  To t2 = f; // 복사 초기화: 모호(ambiguous)
    // 참고: 변환 함수가 non-const 타입에서 온 경우, 예를 들어
    // From::operator To(); 라면 이 경우 생성자 대신 그것이 선택돼요
    
    To t3 = static_cast<To>(f); // 직접 초기화: 생성자 호출
    const To& r = f;            // 참조 초기화: 모호
}

자기 자신의 (cv-한정될 수 있는) 클래스로(또는 그 참조로), 자기 클래스의 기반으로(또는 그 참조로), 그리고 void 타입으로의 변환 함수를 정의할 수는 있지만, 일부 경우 가상 디스패치를 통해서가 아니라면 변환 시퀀스의 일부로 실행될 수는 없어요.

struct D;

struct B
{
    virtual operator D() = 0;
};

struct D : B
{
    operator D() override { return D(); }
};

int main()
{
    D obj;
    D obj2 = obj; // D::operator D()를 호출하지 않음
    B& br = obj;
    D obj3 = br;  // 가상 디스패치를 통해 D::operator D() 호출
}

변환 함수는 멤버 함수 호출 문법으로도 호출할 수 있어요.

struct B {};

struct X : B
{
    operator B&() { return *this; };
};

int main()
{
    X x;
    B& b1 = x;                  // X::operator B&()를 호출하지 않음
    B& b2 = static_cast<B&>(x); // X::operator B&()를 호출하지 않음
    B& b3 = x.operator B&();    // X::operator B&() 호출
}

변환-타입-id는 **탐욕적(greedy)**이에요. 즉, 변환-타입-id를 이룰 수 있는 (있을 경우 속성까지 포함한) 가장 긴 토큰 시퀀스를 의미해요. (C++11부터)

& x.operator int * a; // error: & (x.operator int*) a 로 해석됨
                      //        & (x.operator int) * a 가 아님

operator int [[noreturn]] (); // error: noreturn 속성이 타입에 적용됨

변환-타입-id에 자리표시자 auto를 쓸 수 있어서, 추론된 반환 타입을 나타낼 수 있어요. (C++14부터)

struct X
{
    operator int();   // OK
    operator auto() -> short; // error: 후행 반환 타입은 문법의 일부가 아님
    operator auto() const { return 10; }       // OK: 추론된 반환 타입
    operator decltype(auto)() const { return 10l; } // OK: 추론된 반환 타입
};

참고: 변환 함수 템플릿은 추론된 반환 타입을 가질 수 없어요.

변환 함수는 상속될 수 있고 가상(virtual)일 수도 있지만, static일 수는 없어요. 파생 클래스의 변환 함수는 같은 타입으로 변환하는 경우가 아니면 기반 클래스의 변환 함수를 숨기지 않아요.

변환 함수는 템플릿 멤버 함수일 수 있어요. 예를 들어 std::auto_ptr<T>::operator auto_ptr<Y> 같은 경우예요. 적용되는 특별 규칙은 멤버 템플릿(member template)과 템플릿 인자 추론(template argument deduction)에서 다뤄요.

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 296 C++98 변환 함수가 static일 수 있었음 static으로 선언할 수 없음
CWG 2016 C++98 변환 함수가 반환 타입을 지정할 수 없었지만, 변환-타입-id에는 타입이 존재함 변환 함수의 선언 지정자에 반환 타입을 지정할 수 없음
CWG 2175 C++11 operator int [[noreturn]] ();에서 [[noreturn]]이 noptr-declarator(함수 선언자)의 일부로 해석되는지 변환-타입-id의 일부로 해석되는지가 불명확했음 변환-타입-id의 일부로 해석됨

더 알아보기

  • operator: 연산자 오버로딩 전반.
  • 멤버 템플릿과 템플릿 인자 추론: 변환 함수 템플릿에 적용되는 특별 규칙.