Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
05-17 00:00
관리 메뉴

nomad-programmer

[Proramming/C++] C++ 스타일의 형 변환 본문

Programming/C++

[Proramming/C++] C++ 스타일의 형 변환

scii 2023. 1. 24. 01:24

형 변환을 할 때는 괄호를 사용했다. 이 방법은 C언어에서 사용하던 방식이다. C++에서는 이를 대체할 수 있는 4가지 종류의 형변환 연산자가 추가되었다. 이 연산자들을 살펴보자.

C++ 스타일의 형 변환을 사용해야 하는 이유?

한 마디로 말해 C 스타일의 형변환은 두 가지 문제점이 있다. 

  1. C 스타일의 형 변환은 눈에 잘 띄지도 않고 툴을 사용해서 찾아내기도 힘들다. 왜냐하면 형변환 말고도 괄호를 사용하는 부분이 많기 때문이다. C++ 스타일의 형 변환은 눈에 잘 띄는 외관을 가지고 있기 때문에 형변환을 수행하는 코드를 한눈에 알아 볼 수 있다. 그렇다면 눈에 잘 띄지 않는 것이 왜 단점이 될까?
    그것은 명시적인 형 변환은 언제나 문제의 소지가 있기 때문이다. 명시적인 형변환을 수행한다는 것은 암묵적인 형변환이 불가능하다는 뜻이고 암묵적인 형변환이 불가능하다는 것은 컴파일러가 생각하기에 문제의 소지가 있다는 뜻이다.
  2. C 스타일의 형변환은 형변환의 의도를 구별해내기가 힘들다. C++의 형 변환 연산자는 그 용도에 따라서
    안전한 형변환 (static_cast)
    const 속성을 제거하는 형변환 (const_cast)
    위험한 형변환 (reinterpret_cast)
    클래스 타입간의 형변환 (dynamic_cast)
    등으로 나뉘어져 있다. 그렇기 때문에 C++ 연산자를 사용해서 형변환을 하면 코드를 읽는 사람이 형변환의 의도를 쉽게 알아챌 수 있다. 컴파일러 역시 코드를 작성한 사람의 의도를 파악할 수 있기 때문에 컴파일러가 개발자의 실수를 발견해서 경고해주는 것도 가능하다.

const_cast 연산자

const_cast 연산자는 어떤 타입에서 const 속성이나 volatile 속성을 제거할 때 사용한다. 다음은 const int 타입을  int 타입으로 형변환하는 코드다.

const int ci = 100;
int i = const_cast<int>(ci);

<int>의 의미는 "const int" 타입을 "int" 타입으로 형변환하겠다는 뜻이다. 코드를 보면 알 수 있겠지만 C 스타일의 형변환에 비해 작성자의 의도가 명확하게 드러난다. 컴파일러 역시 const_cast의 의미를 알고 있기 때문에 const 속성을 제거하는 용도 외의 다른 종류의 형변환을 시도하게 되면 오류를 발생시킨다. 

reinterpret_cast 연산자

reinterpret_cast 연산자는 일반적으로 허용하지 않는 위험한 형변환을 할 때 사용한다. 예를 들어 포인터를 정수로 변환하는 등의 작업이 이에 해당한다.

int a, b;
a = reinterpret_cast<int>(&b);

프로그램을 작성하다보면 어쩔 수 없이 이런 종류의 형 변환을 사용할 일이 생기는데 이를 통해 발생하는 문제는 어디까지나 개발자의 책임이다. 개발자 스스로가 안전에 유의할 필요가 있는 것이다.

static_cast 연산자

static_cast 연산자는 가장 일반적인 형태의 형 변환을 할 때 사용한다. 예를 들어 double 타입을 char 타입으로 형변환하는 데 사용할 수 있다.

double d = 3.14;
char c;
c = static_cast<char>(d);

static_cast 는 명시적인 형변환이기는 하지만 대체적으로 안전한 형변환이라고 볼 수 있다. 아마도 수행하는 대부분의 형변환은 static_cast의 범주에 속할 것이다. 구체적으로 어떤 경우에 사용하는지 알고 싶다면 다음과 같은 공식을 따르면 된다.

만약에 A 타입에서 B 타입으로의 암묵적인 형변환이 가능하다면 static_cast를 사용해서 B 타입에서 A 타입으로 형변환할 수 있다. 이를 테면 int* 타입은 void* 타입으로 암묵적인 형변환이 되기 때문에 static_cast를 사용해서 void* 타입을 int* 타입으로 변환할 수 있다.

dynamic_cast 연산자

유일하게 C 스타일의 형변환으로는 흉내낼 수 없는 것이 dynamic_cast다. dynamic_cast는 서로 상속 관계에 있는 클래스간에 형 변환을 할 때 사용한다.
중요한 것은 형변환을 수행하는 동시에 과연 이 형변환이 안전한 것인지까지 검사를 해준다. 예를 들어 3개의 클래스가 다음과 같은 상속 관계를 가지고 있다고 해보자.

#pragma warning(disable: 4996)

#include <typeinfo>
#include <iostream>

using namespace std;

class A {
public:
    virtual void Func() {}
};

class B : public A {
};

class C : public B {
};

int main(void) {
    // C 객체를 생성 A*에 담는다.
    A* pA1 = new C;
    // A 객체를 생성 A*에 담는다.
    A* pA2 = new A;

    // pA1을 C* 타입으로 형변환
    C* pC1 = dynamic_cast<C*>(pA1);         // 성공
    if (pC1 == nullptr) {
        cout << "[error] *pC1, cast" << endl;
    }

    // pA2를 C* 타입으로 형변환
    C* pC2 = dynamic_cast<C*>(pA2);         // 실패
    if (pC2 == nullptr) {
        cout << "[error] *pC2, cast" << endl;
    }

    try {
        // *pA2를 C& 타입으로 형변환
        C& rC1 = dynamic_cast<C&>(*pA2);    // 실패. bad_cast 예외 발생
    }
    catch (bad_cast& ex) {
        cout << "[error] rC1&, cast: " << ex.what() << endl;
    }

    return 0;
}


// 결과
[error] *pC2, cast
[error] rC1&, cast: Bad dynamic_cast!

위에서 본 것처럼 dynamic_cast는 다운 캐스트, 즉 부모 클래스 타입에서 자식 클래스 타입으로 형변환할 때 유용하게 사용할 수 있다. 다운 캐스트는 포인터나 레퍼런스가 가리키고 있는 객체의 실제 타입이 무엇이냐에 따라 안전할 수도 있고 위험할 수도 있다. dynamic_cast가 알아서 안전 여부를 검해주기 때문이다.

만약에 형변환에 문제가 있는 경우라면 dynamic_cast 연산자는 nullptr 값을 반환하거나 bad_cast 예외를 던지게 된다.

포인터의 형변환이라면 nullptr을 반환함으로써 문제 상황을 알리 수 있지만 레퍼런스의 형변환인 경우에는 어떤 특정한 값을 반환하는 것이 불가능하므로 bad_cast 예외를 던진다.

위의 클래스 A를 보면 아무 일도 하지 않는 가상 함수가 하나 있다. 이는 dynamic_cast를 사용하기 위해서 필요한 작업이다. 가상 함수가 하나도 없는 클래스는 dynamic_cast를 사용할 수 없기 때문인데, RTTI의 내부 구현과 관련이 있다. 클래스 B, C는 부모로부터 물려받은 가상 함수가 있는 셈이므로 dynamic_cast를 사용할 수 있는 것이다.


dynamic_cast와 RTTI

RTTI(Runtime Type Information) 는 실행 시간에 객체의 타입에 대한 정보를 얻을 수 있는 기능을 말한다. 위의 예제에서처럼 dynamic_cast 연산자가 pA1이 가리키고 있는 객체가 실제로 A 클래스인지 아니면 C 클래스인지를 알아내 수 있는 것도 다 RTTI 덕분이다.

그런데 비주얼스튜디오는 기본적으로 RTTI 기능을 사용하지 않게 설정되어 있다. 하는 일이 많은 만큼 자체적인 부하(Overhead)를 가지고 있기 때문이다. 그래서 dynamic_cast를 비롯한 RTTI의 기능을 사용하기 위해서는 다음과 같이 프로젝트의 설정을 변경시킬 필요가 있다.

"솔루션 탐색기 > 프로젝트 > 속성" 메뉴를 선택하면 아래 그림처럼 나오는데 "C/C++ > Language" 선택 후 "Enable Run-Time Type Information" 속성을 Yes(/GR) 로 변경한다.


형변환 방법을 컴파일러에게 알려주기

클래스 타입과 기본 타입 간의 형 변환을 가능하게 해주는 방법에 대하여 알아보자. 예를 들어 Complex 타입과 int 타입 간의 형 변환이 암묵적으로 이뤄지게 만들 수 있다.

형 변환 방법을 컴파일러에게 알려주는 이유

컴파일러에게 형변환 방법을 알려줘야 하는 이유는 간단하다. 컴파일러는 그 방법을 모르기 때문이다. 예를 들어 컴파일러는 double 타입을 int 타입으로 형변환하려면 소수점 이하 부분을 잘라내면 된다는 사실을 알고 있다. 하지만 컴파일러는 Complex 타입을 int 타입으로 형변환하는 방법은 모른다. 왜냐하면 Complex 타입은 사용자정의로 만든 것이기 때문이다.

사용자 정의의 클래스를 다른 타입으로 형 변환

사용자 정의로 만든 클래스를 다른 타입으로 형변환하려면 연산자 오버로딩을 통해야 가능하다. 다음의 코드를 보자.

#pragma warning(disable: 4996)

#include <iostream>

using namespace std;

class Complex {
public:
    Complex(int realpart, int imaginarypart)
        :real(realpart), imaginary(imaginarypart) {}

    int Real(int realpart) {
        real = realpart;
        return real;
    }
    int Imaginary(int imaginarypart) {
        imaginary = imaginarypart;
        return imaginary;
    }

    int Real() const { return real; }
    int Imaginary() const { return imaginary; }

    // 형변환 연산자를 정의. 이 함수는 Complex타입을 int타입으로 형변활할 필요가 있는 때 호출한다.
    // 반환값의 타입을 명시하지 않았지만, int타입으로의 형변환이므로 int타입의 값을 반환한다.
    operator int() {
        // 복소수의 실수부를 반환. 이 부분이 바로 Complex타입에서 int타입으로의 형변환 방법을
        // 컴파일러에게 제공하는 부분이라고 볼 수 있다.
        return real;
    }

private:
    int real;
    int imaginary;
};


int main(void) {
    Complex c1(5, 10);

    int i;
    // 이제 Complex객체를 int타입으로 암묵적으로 형변환이 가능하다.
    i = c1;         // i = 5;
    // 위의 코드와 같은 의미이다.
    i = c1.operator int();

    return 0;
}

Complex 클래스에 추가한 함수가 바로 형변환 연산자를 오버로딩한 것이다. 이제 Complex타입을 int타입으로 형변환할 일이 있을 때는 이 함수가 호출된다. 만약 Complex타입을 double타입으로 형변환하게 만들고 싶다면 operator double() 과 같은 원형의 멤버 함수를 추가하면 된다.


다른 타입을 사용자 정의 클래스로 형 변환

int타입을 Complex타입으로 암묵적으로 형변환할 수 있게 만들어보자. 다음의 코드를 보면 알 수 있겠지만 인자를 가진 생성자를 사용하면 된다.

#pragma warning(disable: 4996)

#include <iostream>

using namespace std;

class Complex {
public:
    Complex(int realpart, int imaginarypart)
        :real(realpart), imaginary(imaginarypart) {}

    // int 타입의 인자를 받는 생성자 정의.
    // int 타입을 Complex 타입으로 형변환할 필요가 있는 때는 이 생성자가 호출된다.
    Complex(int i)
        :real(i), imaginary(0) {}

    int Real(int realpart) {
        real = realpart;
        return real;
    }
    int Imaginary(int imaginarypart) {
        imaginary = imaginarypart;
        return imaginary;
    }

    int Real() const { return real; }
    int Imaginary() const { return imaginary; }

    operator int() {
        return real;
    }

private:
    int real;
    int imaginary;
};


int main(void) {
    int i = 5;
    Complex c(0, 0);

    // int타입을 Complex타입으로 암묵적 형변환 가능
    c = i;
    // 위의 코드와 같은 의미이다.
    c = Complex(i);

    return 0;
}

Complex(i) 를 보면 임시 객체를 생성했다고 생각해도 되고, 형변환이 이루어졌다고 생각해도 된다.

어떤 경우에는 암묵적인 형변환이 문제가 되기도 한다. 암묵적인 형변환은 개발자 모르게 수행되기 때문에 예상하지 못했던 상황에서 형변환이 발생해 문제를 일으킬 수도 있기 때문이다.

이런 경우에는 explicit 키워드를 붙여주면 된다. explicit 키워드는 그 이름처럼 '명시적인' 형변환만을 허용한다.

explicit Complex(int i)
    :real(i), imaginary(0) {}

이제는 명시적인 형변환을 사용한 경우에만 int타입을 Complex타입으로 변환할 수 있다. 또한 암묵적인 형변환을 시도한 곳에서는 오류가 발생한다.

int main(void) {
    int i = 5;
    Complex c(0, 0);

    // 암묵적인 형 변환 
    c = i;                          // 에러

    // 명시적인 형 변환
    c = Complex(i);                 // 성공
    c = (Complex)i;                 // 성공
    c = static_cast<Complex>(i);    // 성공

    return 0;
}
Comments