Introduction
앞서 type casting 포스트에서 전반적인 casting에 대한 내용을 다루었습니다. 해당 포스트는 다양한 종류의 type casting 방법들에 대한 일종의 허브 역할로서 계속 업데이트할 예정입니다. 이번 포스트를 읽으신 후 더 많은 내용을 보고 싶다면 아래 URL을 클릭해 주세요.
이번 포스트의 본론으로 돌아와서 static_cast에 대해 이야기해 보겠습니다. 흔히 프로그래밍에서 임의의 type을 다른 type으로 변환하는 것을 type casting이라고 합니다. C++에서는 참 많은 type casting 방법을 지원하는데, 별도의 키워드가 없는 묵시적 방법이나 C style type casting을 제외한 명시적 casting에는 총 5가지의 방법이 있습니다. 그중 이번 포스트에서는 static_cast에 대해 이야기하도록 하겠습니다.
Definition
기본적으로 static_cast는 int, float, double 등의 값이나 struct, class 등의 객체 간 변환에 사용됩니다. 하지만 이런 정의는 넓게는 type casting에도 적용될 수 있습니다. static_cast만의 특별한 점은 무엇일까요?
Converts between types using a combination of implicit and user-defined conversions.
cppreference.com에서는 static_cast의 정의를 위와 같이 정의합니다. 정의에서도 알 수 있듯이, static_cast는 기본적으로 묵시적(implicit) 형 변환의 특성을 일차적으로 가져갑니다. 묵시적 형 변환의 가장 큰 특징은 compile 단에서 변환의 무결성을 검사하는 것이며, 여기서 통과를 하는 경우에만 변환하게 됩니다.
그렇다면 왜 하필 이름이 정적(static) casting일까요? C++에서 정적이라는 용어는 주로 compile 단계에 동작이 결정되는 경우에 사용합니다 (다른 언어는 깊게 알지 못해 C++에 한정시켰지만, 크게 다르지 않을 거라고 생각합니다). 반대로 run-time 중에 동작이 결정되는 함수의 경우 동적이라는 용어를 적용합니다. C++을 처음 접하시는 분들도 알법한 대표적인 예를 들자면, memory size를 미리 할당하는 정적 할당과 run-time에 결정하는 동적 할당이 있습니다.
Syntax
cppreference.com에 명시된 static_cast의 syntax는 다음과 같습니다.
static_cast < new-type > ( expression )
이 code의 결과물로는 new type의 값을 반환합니다. 반올림 오차를 제외한 형 변환 전과 후의 값을 유지하는 게 기본 원칙이나, 이진수 표기는 달라질 수 있습니다.
- float 3.f == 0100 0000 0100 0000
- int 3 == 0000 0000 0000 0011
묵시적 변환과 같이 int, float, double 등 대부분의 type에 대한 casting을 지원합니다. 심지어 사용자 정의 생성자나 변환 루틴에서 허용되는 변환에도 사용 가능합니다. 아래는 int, float, double 등 value와 struct, class 등 객체에 대한 예제 코드입니다.
// from float to int
float myFloat;
int myInt = static_cast<int>(myFloat);
// compile error if you try to convert something into pointer
char *ptrChar = static_cast<char*>(myFloat); // compile error
// from class to class
class Base { ... };
class Derived { ... };
int main()
{
Base base;
Derived derived;
Base *ptr_base{ nullptr };
Derived *ptr_derived{ new Derived };
// using pointer
ptr_base = ptr_derived; // don't need a cast for up-casting
ptr_derived = static_cast<Derived*>(ptr_base); // need a cast for down-casting
// using reference
Base& ref_base{ derived }; // don't need a cast for up-casting
Derived& ref_derived{ static_cast<Derived&>(ref_base) }; // need a cast for down-casting
// nonallowable code
Base *err_base{ new Base() };
Derived *err_derived{ static_cast<Derived*>(err_base) };
return 0;
}
첫 번째 예제 코드에서 알 수 있듯이 static_cast는 pointer에 대한 형 변환에 엄격합니다. Type 관계없이 pointer로의 변환은 compile error를 뱉도록 되어 있죠. 그러나 두 번째 예제 코드를 보면 또 항상 그런 것은 아님을 알 수 있습니다. 객체에 대한 형 변환은 pointer를 거쳐가는 것이 허용됨을 볼 수 있습니다.
여기서 흥미로운 점은 static_cast를 이용한 형 변환은 pointer나 reference를 이용해 적용할 수 있지만, struct나 class 객체 자체를 활용해서는 할 수 없습니다. 뿐만 아니라 up-casting, down-casting이라는 용어가 중간에 나오는데 여기서는 간단히만 알고 가면 될 것 같네요. 물론 중요한 부분이지만 아마 이 글을 읽고 계신 지금, 너무 깊은 내용까지 받아들이기는 힘들 수 있으니까요. 간단하게 아래와 같이 짚고 넘어가도록 하겠습니다. Up/Down casting 관련된 이야기는 추후 자세히 다루도록 하겠습니다.
- Up-casting: 자식 객체에서 부모 객체를 가리키는 행위로 묵시적 형 변환이 가능합니다.
- Down-casting: 부모 객체에서 자식 객체를 가리키는 행위로 묵시적 형 변환이 불가능합니다. Up-casting 된 포인터를 원래의 타입으로 재변환할 때 많이 사용됩니다.
예제 코드 중간에 nonallowable code라고 명시된 부분이 있습니다. 이는 static_cast가 compile 단에서 잡아내지는 못하지만 높은 확률로 run-time error를 뱉어낼 부분입니다. 잠재적인 시한폭탄이라는 것이죠. 이런 현상은 왜 존재하는 걸까요? 이는 static_cast가 명시적 casting의 극히 일부만을 커버하고 있기 때문입니다. 아래에서 좀 더 구체적으로 살펴보도록 하죠.
Constraint
static_cast, 놈은 사천왕 최약체이다
사실 static_cast는 그리 강력한 casting operator가 아닙니다. 기존 C style explicit casting 기능 중 일부만을 가져왔을 뿐입니다. 물론 C style casting이 워낙 괴물 같은 놈이라 다른 casting operator에도 해당되는 이야기이니 패스한다면, 가장 큰 이유는 묵시적 형 변환과 역할을 같이한다는 점입니다. 그렇습니다. 이 녀석은 명시적으로 적지 않아도 괜찮을 정도로 별 기능이 없는 기능을 제공합니다.
포인터 타입이 서로 관련이 없을 때에도 적용이 불가능하며, 변환 생성자가 제공되지 않는 타입의 객체 역시 적용 불가합니다. 또한, const 타입을 해제하지도 못하고, 포인터에 대한 변환이 특수한 경우를 제외하고는 불가능합니다. 가장 큰 문제점은 compile 단에서만 정적으로 형 변환에 대한 type check를 한다는 것입니다. 이로 인해 문법적으로 문제없는 코드가 run-time에서 접근하면 안 되는 (할당되지 않은, 초기화되지 않은) 영역을 침범할 수 있습니다. 이는 debugging으로 찾기도 힘들어 꽤나 골치 아픈 bug로 남게 됩니다 (모든 case에 대해서 뻗지도 않아 더 그런 것 같습니다).
Pointer 변환 역시 일반적으로 허용되지도 않습니다. 그래서 int, float 등에서 pointer로의 변환도 불가능하고, 반대의 경우 역시 마찬가지입니다. 그러나 예외적으로 허용이 되는 경우가 있습니다. 앞에서도 봤듯이 class, struct 형 변환 시에는 허용이 되는데 상속관계가 있거나, void pointer를 다른 type pointer로 변환하거나 반대의 상황에서만 허용됩니다.
이렇듯 static_cast는 꽤나 제약사항이 많은 기능입니다. C style casting에서는 모든 게 허용되었는데 꼭 이 녀석을 써야 할까요? 이 질문에 대한 답변은 C++에서는 강력히 권장합니다 정도로 할 수 있겠습니다. C++ cast operator들은 C style casting을 기능별로 조각을 냈습니다. 그래서 static_cast가 못 하는 기능은 다른 cast operator에서 제공하게 됩니다. 예를 들면, const value를 해제하는 것은 static_cast는 못했지만, const_cast는 할 수 있습니다. 또한, run-time에서의 type check의 경우 static_cast는 못 하는 반면, dynamic_cast는 할 수 있습니다. 이렇게 C++은 기능별로 다른 cast operator를 제공해 개발자의 의도를 파악할 수 있게 하며, debugging을 비롯해 여러 작업을 수월하게 도와줍니다.
Summary
Description | Yes/No | Remark |
int, float, double, etc → int, float, double, etc | Yes | |
Base class pointer → Base class pointer | Yes | |
Base class pointer → Derived class pointer | Yes | can cause run-time error |
Derived class pointer → Base class pointer | Yes | |
Derived class pointer → Derived class pointer | Yes | |
pointer → int, float, double, etc | No | compile error |
int, float, double, etc → pointer | No | compile error |
Reference