C, C++/C++ 언어 / / 2024. 10. 1.

[C++] 사용자 정의 타입( user defined type )을 다른 타입으로 변경하기

자연스러운 발상

explicit 키워드를 설명하기 위해, 변환 생성자( converting constructor )가 무엇인지를 얘기했었습니다.

기억이 가물해졌거나, 처음 접하는 분들은 이 글을 읽어보시길 추전 합니다.

 

[C++] explicit 키워드와 변환 생성자( converting constructor )

변환 생성자( Converting Constructor )가 무엇인가변환 생성자는 암시적인 타입 변환을 통해 호출되는 생성자를 말합니다.그리고, 이 생성자를 통해서 임시 객체가 생성됩니다. 먼저, 암시적인 타입

codingembers.tistory.com

 

이 변환 생성자를 이용하면, 아래와 같은 코드가 가능합니다.

class IntegerObj{
    int m_val;
public:

    IntegerObj(int val) : m_val(val){}	// 변환 생성자

    int getVal() const{
        return m_val;
    }
};

int main(){
	
    IntegerObj val = 5;	// ok
}

클래스의 이름( Integer Obj )을 보면, 이러한 코드는 자연스럽게 보여집니다.

그런데, 이 객체의 값을 출력하려면 다음과 같이 해야 합니다.

int num = 5;
IntegerObj valObj = 5;

std::cout << num << valObj.getVal() << endl;

num = valObj.getVal();	// 다른 int 변수에 대입

멤버 함수 getVal을 통해야, 객체가 가지고 있는 값을 출력할 수 있는 것이죠.

그리고, int 값을 대입 연산자   =   를 통해서 바로 받은 것과 달리, int 변수에 객체의 값을 대입하려면, 역시 멤버 함수를 통해야 됩니다.

 

만약, 다음과 같은 기능을 클래스에 추가하려면 어떻게 해야 될까요?

IntegerObj valObj = 5;
int num = valObj;

 

연산자 오버로딩( overloading )을 통한 암시적인 변환

위와 같은 기능은 연산자 오버로딩을 통해 구현할 수 있습니다.

오버로딩은 같은 함수 식별자에 대해, 다양한 타입의 매개변수를 지정할 수 있게 하는 C++의 기능입니다.

class IntegerObj{
    int m_val;
public:

    IntegerObj(int val) : m_val(val){}

    operator int () const{  // 연산자 오버로딩
        return m_val;
    }
};

이와 같은 기능은 변환하고자 하는 타입을 반환하는 연산자를 구현하는 것으로 가능합니다.

이러한 연산자를 변환 연산자( converting operator )라고 합니다.

이제, 이전 항목에서 객체를 출력할 때 사용했던 방법을 다음과 같이 수행할 수 있습니다.

IntegerObj valObj = 5;
std::cout << valObj << endl;

int num = valObj;

 

그리고, int, char 같은 원시( primitive ) 타입이 아닌 다른 사용자 정의 타입으로의 변환도 가능합니다.

아래는, IntergerObj과 비슷하게 double 타입의 데이터를 가진 클래스입니다.

class DoubleObj{
    double m_val;
public:

    DoubleObj(int val) : m_val(val){}
    DoubleObj(double val) : m_val(val){}
    
    operator double () const {
        return m_val;
    }
};

int main(){
    IntergerObj intObj = 5;
    double dVal = intObj;	// ok
    DoubleObj dObj = intObj;	// error
}

위 main 함수의 두 번째 문장은 합법입니다.

이것은, IntegerObj는 int 타입으로 변환이 가능합니다.

그리고, int 타입은 C++의 숫자 변환 규칙에 의해 double 타입으로 변환되기 때문입니다..

 

또한, 맨 마지막 문장도 문제가 없어 보입니다.

IntegerObj가 int로 변환되고, int는 변환 생성자를 통하여 DoubleObj로 변환되기 때문입니다.

하지만, 맨 마지막 문장은 잘못된 문장입니다. 왜일까요?

 

이 글을 시작할 때 링크한 글에서 설명한 것과 같이, 사용자 타입의 변환은 한 번만 가능합니다.

즉, intObj가 int 타입으로 변환되고, 이 int 타입의 값이 다시 DoubleObj 타입으로 변환될 수는 없습니다.

하지만, 이러한 연쇄 변환이 안된다면, IntergerObj에서 DoubleObj 타입으로의 변환을 구현하면 됩니다.

class IntegerObj{
    int m_val;
public:

    IntegerObj(int val) : m_val(val){}

    operator int () const{  // 연산자 오버로딩
        return m_val;
    }

    // DoubleObj 타입으로의 변환
    operator DoubleObj () const{
        return DoubleObj(m_val);
    }
};

int 타입이 double 타입으로 변환하는 것과 마찬가지로, IntegerObj 타입이 DoubleObj 타입으로 변환됩니다.

그리고, 이러한 클래스 타입의 객체들은 원시 타입으로도 변환할 수 있게 되었습니다.

int main(){

    IntegerObj valObj = 5;
    std::cout << valObj << endl;    // ok

    int num = valObj;           // ok
    double dVal = valObj;       // ok
    DoubleObj dObj = valObj;    // ok
}

이제 모든 것이 원하는 대로 되었습니다.

 

이러한 암시적 변환이 많이 쓰이는 곳은  if 구문입니다.

if ( 표현식 ) 형태의 구문을 수행할 때, 컴파일러는 괄호 안의 표현식을 bool 타입으로 변환하려고 시도합니다.

 

다음 코드가 대표적인 예 중의 하나입니다.

int main(){

    double dVal;
    cin >> dVal;
    if ( !cin ){	// bool 타입으로 암시적 변환
        cout << "error occured !!";
    }
    else{
        cout << "valued wad inputed: " << dVal << endl;
    }
}

▼출력

a
error occured !!

위 예문에서 'a'를 입력하면, 타입이 다르기 때문에, std::cin 객체는 내부의 상태 플래그를 오류로 설정합니다.

그리고, 컴파일러는 cin 객체를  bool 타입으로 변환하기 위해, 이 내부 플래그를 판단해서 false를 반환하는, 오버로딩된 연산자   operator bool ()   를 호출한 것입니다.

이러한 코드는 cin 객체( 사실 cin은 std::istream 클래스 타입의 전역 객체 )의 멤버 함수인 flags() 나 fail() 같은 함수를 호출하지 않고도 목적을 달성합니다.

 

예를 하나 더 들어보겠습니다.

optional<double> divide( int x, int y){
    
    if ( y == 0)
        return {};  // 분모가 0이면 함수 실패
    else{
        return static_cast<double>(x) / y;
    }
}

int main(){

    optional<double> ret = divide( 10 , 0);
    if ( ret){	// bool 타입으로 변환
        cout << "result: " << *ret1 << endl;
    } 
    else{
        cout << "division failed\n";
    }
}

위의 divide 함수는 std::optional 객체를 반환합니다.

이 optional 객체 역시   operator bool ()   을 구현해 두었습니다.

이 함수는 optional 객체가 값을 갖고 있으면 true를 반환하는 변환 연산자입니다.

그래서, 위와 같이 간단히 함수의 결과를 판단하는 코드   if ( ret ) { ... }   를 작성할 수 있는 것입니다.

 

std::optional에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++ 17] 함수의 결과와 값을 동시에 알려주는 optional 객체

함수의 반환 값함수를 사용하다 보면 많은 경우, "함수가 성공적으로 수행됐고, 그 결과 값이 어떻게 되었다"라는 내용을 함수 호출자에게 알려야 할 때가 있습니다. 이러한 문제를 해결하기 위

codingembers.tistory.com

 

명시적 변환( explicit conversion )

위에서 예로 들었던 IntegerObj 가지고 계속 설명하겠습니다.

이 IntegerObj 설계할 때, int 타입이나 double 타입으로의 암시적인 변환을 의도하지 않을 수도 있습니다.

대신, DoubleObj로의 암시적인 변환만을 지원하려고 하는 것이죠.

 

다음의 코드는 위의 상황을 보여줍니다.

// DoubleObj 타입의 매개 변수를 받는 출력 함수
void printVal(const DoubleObj& obj){
    cout << obj << endl;
}

int main(){

    double dVal = 3.14;
    printVal( dVal );   // double 타입으로 출력할 수 없도록 수정 필요.

    IntegerObj intObj = 5;
    cout << intObj << endl; // printVal 함수를 통해야만 출력하기를 원함

    printVal( 5 );      // 당연히 안되어야 합니다.

    printVal( intObj ); //  이 방식의 출력만 지원
}

이 항목의 목적은, 원시 타입의 데이터는 printVal 함수를 통해서 출력하지 못하고, IntegerObj는 원시 타입으로의 암시적인 변환을 막아서, printVal를 통해서 출력하도록 만드는 것입니다.

 

그러려면, 먼저 DoubleObj의 생성자가 변환 생성자( conversion constructor )가 되지 않도록 만드는 것입니다.

( double 값을 바로 받을 수 없도록 )

이것은 다음과 같이 explicit를 사용함으로써 구현됩니다.

class DoubleObj{
    double m_val;
public:

    explicit DoubleObj(int val) : m_val(val){}  // 명시적 전환만 지원
    explicit DoubleObj(double val) : m_val(val){}

    operator double () const {
        return m_val;
    }
};

 

그리고, IntergerObj는 암시적으로 int 타입으로 변환되선 안됩니다.

class IntegerObj{
    int m_val;
public:

    IntegerObj(int val) : m_val(val){}

    explicit operator int () const{  // 명시적 전환만 지원
        return m_val;
    }

    // DoubleObj 타입으로의 변환
    operator DoubleObj () const{
        return DoubleObj(m_val);
    }
};

이것은, 변환 연산자( conversion operator )에도 explicit 키워드를 사용함으로써 구현됩니다.

이렇게 하면, int 타입으로 암시적인 변환이 되지 않습니다.

참고로, 다른 함수에는 이러한 explicit 키워드를 사용할 수 없습니다.

 

이 두 가지만 수정하는 것으로 모든 것이 원하는 대로 제한되고, 출력 방식을 통제할 수 있게 됩니다.

int main(){

    double dVal = 3.14;
    printVal( dVal );   // error !

    IntegerObj intObj = 5;
    cout << intObj << endl; // error !

    printVal( 5 );      // error !

    printVal( intObj ); //  ok
}

그렇다고 해서, 이 방식이 명시적인 변환까지 막는 것은 아닙니다.

변환 연산자를 구현했으므로, 명시적인 변환은 당연히 가능합니다.

// IntegerObj에 구현했었던 변환 연산자
explicit operator int () const{  // 명시적 전환만 지원
    return m_val;
}
// ...

cout << static_cast<int>(intObj) << endl;	// 명시적인 전환 ok

 

또한, 이러한 타입 변환 없이 std::cout를 통해 직접 IntegerObj를 출력하는 방법을 찾는다면, 삽입 연산자   <<   를 오버로딩하는 방법도 있습니다.

class IntegerObj{
    int m_val;
public:

    IntegerObj(int val) : m_val(val){}

    explicit operator int () const{  // 명시적 전환만 지원
        return m_val;
    }

    operator DoubleObj () const{
        return DoubleObj(m_val);
    }

    // private 멤버에 접근할 수 있도록
    friend ostream& operator << ( ostream& os, const IntegerObj& obj);
};

// << 연산자 오버로드
ostream& operator << ( ostream& os, const IntegerObj& obj){
    os << obj.m_val;
    return os;
}
// ...

cout << intObj << endl;     // ok

명시적인 변환 없이도, C++의 입출력 기능을 직접 사용할 수 있습니다.

 

사용자 정의 출력에 대한 자세한 내용은 여기에서 볼 수 있습니다.

 

[C++] 오버로딩( overloading )을 통한 사용자 정의 데이터 입출력

사용자 정의 데이터를 입출력 스트림과 주고받기이 글에서는 C++의 입출력 클래스를 통해, 사용자 클래스의 데이터를 출력하고, 다시 입력받는 기능을 추가해 보겠습니다.다음은 2차원 좌표 테

codingembers.tistory.com

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유