ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++] 형 변환 연산자
    C++/이것이 C++이다 2021. 9. 19. 21:21

    지난 글에서는 가상 함수에 관해 살펴보았습니다.

    C++과 객체 지향이라는 두 키워드를 같이 놓고 본다면, 가상 함수는 가장 중요한 내용 중 하나일 것 입니다.

    그런 가상 함수의 무게를 줄이지 않기 위해 이번 글과 분리 해 놓았습니다.

    이번 글에서는 C++에서 제공하는 형 변환 연산자에 대해 살펴 볼 것입니다.

     


     

    1. 개요

     

    더보기

    형 변환 연산자는 이전 글(깊은 복사와 얕은 복사)에서 잠시 언급되었습니다.

    다시 짚어보자면, C++의 형 변환 연산자는 4 종류가 존재합니다.

    const_cast<>
    static_cast<>
    dynamic_cast<>
    reinterpret_cast<>

    C언어의 강제 형 변환 연산자의 위험성과 불안정성을 해결하기 위해 C++에서 새로 정의된 4가지 형 변환 연산자입니다.

     

    다음 문단에서 위 연산자들을 살펴보도록 하겠습니다.

     

    2. const_cast<>

     

    더보기

    const_cast<>는 상수형 포인터(참조)의 상수성 (const)을 제거하는 연산자입니다.

    이전 글(클래스 기본 문법 2)에서 언급했듯이 설계상의 안정성을 깨뜨리는 방법이 될 수 있으며 사용에 있어 충분히 고려하고, 납득할만한 이유가 있을 때 사용해야 할 것입니다.

     

    예제를 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    int main() {
        const int &a = 10;
        int& b = const_cast<int&>(a);
        // Complie error
        //a = 8; 
        cout << a << "\n****\n";
    
        b = 8;
        cout << a << "\n";
        cout << b << "\n";
            
        return 0;
    }
    10
    ****
    8
    8

    a는 상수로 선언되었기 때문에 변경이 불가능하지만 const_cast 연산자를 이용할 경우 상수성을 제거하여 수정이 가능해집니다.

     

    여기서 유의할 점은, const_cast연산자는 상수형 포인터의 상수성을 제거하는 것이입니다.

    잘못된 예제를 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    int main() {
        const int a = 10;
        int& b = const_cast<int&>(a);
        // Complie error
        //a = 8; 
        cout << a << "\n****\n";
    
        b = 8;
        cout << a << "\n";
        cout << b << "\n";
            
        return 0;
    }
    10
    ****
    10
    8

    포인터가 아닌 상수에 대해서는 의도대로 값이 변하지 않는 것을 볼 수 있습니다.

     

    3. static_cast<>

     

    더보기

    상속 관계에서, 파생 형식과 추상 형식은 서로 형변환이 가능합니다.

    이 때, 파생 형식이 추상 형식으로 변환되는 것을 상향 형변환 (업캐스팅) 이라고 하고

    추상 형식이 파생 형식으로 변환되는 것을 하향 형변환 (다운캐스팅) 이라고 합니다.

    업캐스팅은 묵시적으로 이루어질 수 있지만, 다운캐스팅은 명시적으로 지정되어야 합니다.

     

    우선 다운캐스팅의 예제입니다.

    #include <iostream>
    using namespace std;
    class MyClassAnc {
    public:
        void set_data(int data) { my_data = data; }
        void print_data() {
            cout << "My data : " << my_data << "\n";
        }
    protected:
        int my_data;
    };
    
    class MyClassDes : public MyClassAnc {
    public:
        void set_data(int data) {
            if (data > 10)
                data = 10;
            
            MyClassAnc::set_data(data);
        }
    };
    
    int main() {
        MyClassAnc* a = new MyClassDes;
        MyClassDes* b = nullptr;
        a->set_data(15);
    
        b = static_cast<MyClassDes*>(a); // Line 28
        b->print_data();
    
        delete a;
    
        return 0;
    }
    15

    Line 28에서, MyClassAnc가 MyClassDes로 다운캐스팅 되었습니다.

    이 부분에서 static_cast를 제거하면 컴파일 에러가 발생할 것 입니다.

    이와 같이, 상속 관계에서 다운캐스팅은 명시적인 형 변환 연산자를 사용해야 합니다.

     

    차후 객체 지향에 관한 글을 따로 쓰면서 다루겠지만, 상속 관계에서 클래스는 하위 클래스로 갈 수록 확장되는 경향이 있습니다.

    다운캐스팅은 이러한 상황에 오류를 발생시킬 수 있는데, 예제를 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyClassAnc {
    public:
        void set_data(int data) { my_data = data; }
        void print_data() {
            cout << "My data : " << my_data << "\n";
        }
    protected:
        int my_data;
    };
    
    class MyClassDes : public MyClassAnc {
    public:
        void set_data(int data) {
            if (data > 10)
                data = 10;
            
            MyClassAnc::set_data(data);
        }
        void print_data() {
            cout << "My data : " << my_data << "\n";
            cout << "New my data : " << new_my_data << "\n";
        }
    protected:
        int new_my_data = 5;
    };
    
    int main() {
        MyClassAnc* a = new MyClassAnc;
        MyClassDes* b = nullptr;
        a->set_data(15);
    
        b = static_cast<MyClassDes*>(a);
        b->print_data();
    
        delete a;
    
        return 0;
    }
    My data : 15
    New my data : -33686019

    위 예제와 달라진 점은 세 가지 입니다.

    1. MyClassDes에서 print_data()메소드 오버라이드
    2. MyClassDes의 멤버 변수 추가
    3. main함수의 변수 a의 실 형식이 MyClassDes에서 MyClassAnc로 변경

    이 예제를 통해 다운캐스팅이 완벽하지 않다는 것을 알 수 있습니다.

    자료형은 약속입니다.

    이것을 의도적으로 변경할 때에는 충분한 이유가 동반되어야 할 것입니다.

     

    4. dynamic_cast<>

     

    더보기

    dynamic_cast는 조금 더 안전한 형 변환에 이용됩니다.

    하지만, 안전함을 대가로 성능을 희생시킨 형 변환이고, 대체할 수 있는 방법도 있기 때문사용을 권장하지는 않는다고 합니다.

     

    우선 예제를 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyClassAnc {
    public:
        void set_data(int data) { my_data = data; }
        virtual void print_data() {
            cout << "My data : " << my_data << "\n";
        }
    protected:
        int my_data;
    };
    
    class MyClassDes : public MyClassAnc {
    public:
        void set_data(int data) {
            if (data > 10)
                data = 10;
            
            MyClassAnc::set_data(data);
        }
        void print_data() {
            cout << "My data : " << my_data << "\n";
            cout << "New my data : " << new_my_data << "\n";
        }
    protected:
        int new_my_data = 5;
    };
    
    int main() {
        MyClassAnc* a = new MyClassAnc;
        MyClassDes* b = nullptr;
        a->set_data(15);
    
        b = dynamic_cast<MyClassDes*>(a);
        cout << "?\n";
        if (b == nullptr)
            cout << "!\n";
    
        delete a;
    
        return 0;
    }
    ?
    !

    윗 문단의 오류가 발생하는 다운캐스팅 예제에서, static_cast를 dynamic_cast로 바꾼 예제입니다.

    dynamic_cast는 변환에 실패하면 nullptr을 반환합니다.

     

    이제 dynamic_cast의 작동 방식에 대해 알아보도록 하겠습니다.

    dynamic_cast는 형 변환에 있어 클래스의 type_info를 판단 지표로 사용하게 됩니다.

    이것은 이전 글(가상 함수)에서 보았던 vtable에 있는 정보입니다.

    따라서 dynamic_cast는 가상 함수가 존재하는 클래스에 한해서만 사용할 수 있습니다.

    이러한 자료형 확인 방법을 RTTI(Run-Time Type Information or Identification)라고 합니다.

     

    이번에는 다운캐스팅에 있어 dynamic_cast보다 효율적인 방법에 대해 극단적인 예제를 통해 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyShape {
    public:
        virtual void draw() { cout << "MyShape::draw()\n"; }
    };
    
    class MyCircle : public MyShape {
    public:
        virtual void draw() { cout << "MyCircle::draw()\n"; }
    };
    
    class MyTriangle : public MyShape {
    public:
        virtual void draw() { cout << "MyTriangle::draw()\n"; }
    };
    
    int main() {
        cout << "1 : Circle, 2 : Triangle\n";
        int flag;
        cin >> flag;
    
        MyShape* shape = nullptr;
        if (flag == 1)
            shape = new MyCircle;
        else
            shape = new MyTriangle;
    
        shape->draw(); // Line 30
    
        cout << "*********************\n";
    
        // Line 34
        MyCircle* circle = dynamic_cast<MyCircle*>(shape);
        if (circle != nullptr)
            circle->draw();
        else {
            MyTriangle* triangle = dynamic_cast<MyTriangle*>(shape);
            triangle->draw();
        }
    
        return 0;
    }
    1 : Circle, 2 : Triangle
    1
    MyCircle::draw()
    *********************
    MyCircle::draw()

    Line 30의 코드가 가상 함수를 이용했을 때의 방법이고 

    Line 34이후의 코드가 dynamic_cast를 이용했을 때의 방법입니다.

    위의 소스코드는 이전 글(가상 함수)에서 보았던 추상 자료형에서의 가상 함수 활용법과 유사합니다.

    가상 함수로 선언하면 간단해지고, 성능도 더 좋은 코드를 가독성도 떨어뜨리고, 성능까지도 안 좋아지는 코드로 선언할 필요는 없을 것 입니다.

     

    물론 위의 예제는 극단적인 경우입니다.

    dynamic_cast가 반드시 필요한 상황에는 사용하는 것이 맞습니다.

    하지만 RTTI가 성능 측면에서 좋지 않음을 기억하고, 다른 방법이 있는지 생각해보도록 하는것이 좋겠습니다.

     

    5. reinterpret_cast<>

     

    더보기

    윗 문단의 static_cast를 다룰 때에는 상속 관계와 업캐스팅, 다운캐스팅 위주로 설명을 했습니다.

    여기에서 첨언하자면, static_cast는 피연산자에 대한 적절한 형 변환 연산자를 호출합니다.

     

    예제를 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    class A {
    
    public:
        int data = 10;
    };
    
    class B {
    public:
        operator A() {
            cout << "Cast\n";
            A a;
            return a;
        }
    };
    
    int main() {
        B b;
        A a = static_cast<A>(b);
    
        return 0;
    }
    Cast

    간단하게 만들어진 클래스 A, B가 있고, B에는 A로의 형 변환 연산자가 존재합니다.

    main함수에서 A 인스턴스를 생성하기 위해 B를 static_cast하여 생성했을 때, 형 변환 연산자가 호출되는 예제입니다.

     

    reinterpret_cast는 이것보다 조금 더, 어쩌면 조금 많이 강력한 형 변환 연산자 입니다.

    C의 형 변환 연산자와 비슷하게 강제성이 있다고 볼 수 있습니다.

     

    예제를 살펴보도록 하겠습니다.

    #include <iostream>
    using namespace std;
    
    int main() {
        int* a = new int(100);
        float* b = reinterpret_cast<float*>(a);
        char* c = reinterpret_cast<char*>(b);
        
        cout << *a << "\n";
        cout << bitset<32>(*a) << "\n";
        cout << *b << "\n";
        cout << bitset<32>(*b) << "\n";
    
        cout << "\n";
        for (size_t n = 0; n < sizeof b; ++n)
            cout << bitset<8>(c[n]);
        
        return 0;
    }
    100
    00000000000000000000000001100100
    1.4013e-43
    00000000000000000000000000000000
    
    01100100000000000000000000000000

    예제의 출력에 의하면, reinterpret_cast는 비트 단위의 형 변환 연산자 입니다.

    출력을 보면, int*형 100을 float*형으로 reinterpret_cast했을 때, 100과는 전혀 관련 없는 값이 출력됨을 볼 수 있습니다.

    하지만 이것을 이진수로 변환해서 보면 동일하다는 것을 알 수 있습니다.

    float*의 이진수 출력이 뒤집혀 있는 것은 출력을 0번 비트부터 했기 때문입니다.

     

    reinterpret_cast의 사용과 관련된 Stackoverflow의 글 하나를 첨부하도록 하겠습니다.

    https://stackoverflow.com/questions/573294/when-to-use-reinterpret-cast

     

    When to use reinterpret_cast?

    I am little confused with the applicability of reinterpret_cast vs static_cast. From what I have read the general rules are to use static cast when the types can be interpreted at compile time henc...

    stackoverflow.com

    static_cast와 reinterpret_cast의 비교를 메모리 주소 관점에서 비교한 답변과 필요할 때 알게 될 것이니 사용하지 말라는 답변을 볼 수 있습니다.

     

    전체적으로 Low한 (메모리, 비트 관점에서 바라보아야 하는) 연산자이기 때문에

    사용 예가 많지 않음과, 사용에 신중을 기해야 하는 연산자임을 알 수 있습니다.

     


     

    C++에서 이렇게 다양한 종류의 형 변환 연산자를 지원하는 이유는 안정성 때문일것입니다.

    형 변환에 있어서는 C언어의 강제 형 변환 연산자가 가장 강력하고 편하겠지만

    그만큼 버그도 많이 발생할 수 있기 때문에, 상황별로, 기능별로 분리해둔 것이라 생각합니다.

    변환에 있어서는 조금 불편하더라도 C++의 연산자를 활용하는 습관을 들이고

    그 이전에, 변환을 하는 충분한 이유가 뒷받침 될 수 있도록 해야 할 것입니다.

     

    읽어주셔서 감사합니다.

    'C++ > 이것이 C++이다' 카테고리의 다른 글

    [C++] 객체 간 관계  (0) 2021.10.19
    [C++] 다중 상속  (0) 2021.09.24
    [C++] 가상 함수  (0) 2021.09.05
    [C++] 상속 기본  (0) 2021.08.13
    [C++] 연산자  (0) 2021.07.16

    댓글

Designed by Tistory.