-
[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
위 예제와 달라진 점은 세 가지 입니다.
- MyClassDes에서 print_data()메소드 오버라이드
- MyClassDes의 멤버 변수 추가
- 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
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