[C++] Cast 4종류 (static, dynamic, const, reinterpret)
static_cast<> : 정적 캐스팅
static_cast는 컴파일 타임에 형변환이 가능한지를 검사하고 캐스팅하는 방식입니다.
컴파일 타임에만 타입검사를 수행하고, 실행 시간(런타임)에는 타입 검사를 수행하지 않습니다.
예를 들어, 정수를 실수로 변환하거나, 기본 클래스 포인터를 파생 클래스 포인터로 변환하는 등의 작업을 수행할 수 있습니다. static_cast는 컴파일러가 타입 안전성을 검사하므로, 불가능한 캐스팅을 시도하는 경우에 컴파일 오류를 발생시킵니다.
> 추가 설명
- 포인터의 타입이 서로 관련 없을때는 static_cast를 적용할 수 없다.
- 변환 생성자가 제공되지 않는 타입의 객체에는 static_cast를 적용할 수 없다.
- 기본적으로 C++의 타입 규칙에서 허용하지 않는 것은 모두 적용할 수 없다고 보면 된다.
dynamic_cast<> : 동적 캐스팅
dynamic_cast는 런타임에 형변환이 가능한지를 검사하고 캐스팅하는 방식입니다.
같은 상속 계층에 속한 타입끼리 다운 캐스팅할 때 런타임에 검사를 수행하게 됩니다. 따라서 포인터나 레퍼런스를 캐스팅할 때 dynamic_cast를 사용할 수 있습니다.
> 런타임 캐스팅 검사
1. 형변환을 수행하려는 객체의 실제 타입을 확인합니다. 이때, 객체의 가상함수테이블에 저장된 RTTI 정보를 이용하여 검사합니다.
2. 형변환 대상 타입과 실제 타입이 일치하거나, 실제 타입의 하위 타입(다운캐스팅)이라면 형변환을 수행합니다.
3. 그렇지 않은 경우엔 형변환을 수행하지 않고 반환됩니다.
만약, 런타임 중에 타입검사를 수행해서 캐스팅하는 것이 적합하지 않다고 판단되면(=캐스팅 실패), 포인터에 대해서는 NULL 포인터를 리턴하고 레퍼런스에 대해선 std::bad_cast 예외를 발생시킵니다.
> dynamic_cast 레퍼런스 예외 발생 예시
int main() { Base base; Derived derived; Base& br{ base }; try { Derived& dr{ dynamic_cast<Derived&>(br) }; } catch (const std::bad_cast&) { std::cout << "Bad cast!" << std::endl; } }
- static_cast나 reinterpret_cast도 같은 상속 계층의 하위 타입으로 다운 캐스팅할 수 있다. dynamic_cast와의 reinterpret_cast의 차이점은, 런타임에 타입 검사를 수행하여 실행 시간에 문제가 되는 타입도 그냥 캐스팅해버린다는 것이다.
타입정보 vtable 저장
dynamic_cast에 필요한 실행 시간의 타입 정보(RTTI)는 객체의 vtable에 저장됩니다. 따라서, dynamic_cast를 적용하려면, 클래스에 virtual 메소드가 최소한 한 개 이상 있어야 합니다. 그렇지 않은 경우엔 컴파일 에러가 발생합니다.
특징
dynamic_cast는 다형성(상속관계)을 띄지 않은 객체 간 변환이 불가능하고, 시도 시 컴파일 에러가 발생합니다.
(다양한 에러 예제는 아래 참조)
또한 RTTI에 의존적이고, 런타임 시간에 실제로 해당 타입이 다운 캐스팅 가능한지 검사하기 때문에 변환 비용이 비쌉니다.
const_cast<> : 상수 캐스팅
const_cast는 const로 선언된 변수를 const 제한으로부터 해제시키기 위해 사용하는 캐스팅입니다.
(사실상 코드상에 사용하는데 위험이 있어 잘 사용하지 않음)
> as_const<>
as_const<>
C++17부터 utility 헤더에 정의된 헬퍼 메소드로, 레퍼런스 매개변수를 const 레퍼런스 변수로 변환해줍니다.
reinterpret_cast<> : 임시 캐스팅
reinterpret_cast는 포인터가 다른 포인터 형식으로 변환될 수 있도록 캐스팅하는 방식입니다. 또한 정수 계열의 형식이 포인터 형식으로 변환될 수 있도록 하고, 그 반대로도 변환될 수 있도록 합니다.
static_cast보다 더 강력하지만 안전성은 조금 떨어지는 캐스팅으로 볼 수 있습니다.
reinterpret_cast는 C++ 타입 규칙에서 허용하지 않는 방식이어도 상황에 따라 캐스팅을 할 수 있습니다. 런타임에 메모리를 직접 재해석하는 방식으로 타입 변환을 수행하기 때문에, 잘못 사용하면 예측할 수 없는 결과를 초래합니다.
예를 들어, 정수형을 포인터형으로 바꾸거나 반대인 포인터형을 정수형으로 바꾸는 등의 캐스팅이 가능합니다. 이때 정수값은 포인터의 절대 주소로 들어가게 되어 위험할 수 있습니다.
정리하자면, reinterpret_cast를 통해 캐스팅할 수 있는 경우는 아래와 같다.
1. 포인터 ↔ 포인터
2. 포인터 ↔ 정수형
3. 정수형 ↔ 포인터
포인터형이 없이 정수형 ↔ 정수형은 컴파일 에러 발생
그러나 int*를 char로 바꾸거나 char을 int*로 바꿀수는 없습니다. (int* -> char -> int* 변환 시 프로그램 터짐)
reinterpret_cast는 형변환이 이뤄질 때 형변환되는 자료형을 새로운 자료형의 bit 수에 맞게 저장하기 때문입니다. char형은 1바이트 크기여서 int* 의 주소값을 전부 표현하지 못하게 됩니다.
이는 자료 그대로를 bit 단위인 변수로 전달한다는 reinterpret_cast의 특징 때문입니다.
따라서 int*를 unsigned int로 바꾸거나, long을 int*로 바꾸는 것 같은 정수형↔ 포인터형의 캐스팅은 성공적으로 수행됩니다.
> 추가 설명
- 서로 관련이 없는 레퍼런스끼리 변환할 수 있다.
- 상속 계층에서 아무런 관련이 없는 레퍼런스간에도 변환할 수 있다. 이러한 포인터는 보통 void* 타입으로 캐스팅한다.
- 정수형 ↔ 포인터형의 캐스팅 시에는 정수형의 크기가 포인터를 담을 정도로 충분히 커야 한다.
정수형의 자료가 너무 작으면 데이터를 옮기는 과정에 dump 되어 원본 데이터가 파괴되고 컴파일 에러가 발생한다.
이해가 어려워서 주절주절 설명하자면... int* p = new int(100); 이라는 포인터 변수를 선언해서 100이라는 정수형을 p에 저장해놓고, char c = reinterpret_cast<char>(p); 처럼 char형 변수 c에 캐스팅하는 상황을 보자.
나는 p에 저장해놓은 100이라는 정수형이 형변환될 줄 알았는데, p의 주소값인 0x00EA5100 이 변수 c에 전달되어 캐스팅 된다.
그러나, 변수 c는 char형이기 때문에 1바이트의 데이터만 담을 수 있어 주소값의 일부분이 잘리게 되어 컴파일 할 때마다 다른 결과가 도출된다.
(사실상 코드상에 사용하는데, 타입검사를 하지 않고 변환하는 위험이 있어 잘 사용하지 않음)
static_cast 와 dynamic_cast를 각각 언제 사용하는가?
static_cast는 이미 어떤 객체로 형변환 되는지를 미리 알고 있는 경우 사용하고, dynamic_cast는 변환될 타입을 한번 검사해봐야 하는 경우 사용합니다.
즉, RTTI(런타임 형식 정보)를 얻어야 하는 경우엔 dynamic_cast를 사용하고, 그렇지 않은 경우엔 static_cast를 사용하여 변환 비용을 줄이는 것이 좋습니다.
따라서 보통 부모클래스를 자식클래스로 다운캐스팅할 때는 dynamic_cast를 사용하고, 자식클래스를 부모클래스로 업캐스팅할때는 static_cast를 사용합니다.
다운캐스팅 시에는 부모클래스가 결국 어떤 포인터를 가르키고있는지가 중요하다. Animal의 부모클래스가 new Animal()을 통해 생성되었는지, 자식클래스인 new Dog()를 통해 생성되었는지에 따라 캐스팅의 성공과 실패가 나뉜다.
결과적으론 new Dog()를 통해 생성해야 Dog 클래스를 포인터가 가르키게 되고, 해당 타입으로 포인터를 생성할 수 있다.
static_cast와 reinterpret_cast의 차이점
static_cast는 안전성 검사를 수행하여 형변환되지 않는 작업을 수행하려 하면 컴파일 에러를 발생시킵니다. 반면 reinterpret_cast는 안전성 검사를 수행하지 않기 때문에 잘못 사용하여 예측할 수 없는 결과를 낼 수 있습니다.
reinterpret_cast를 활용하는 예시
마이크로소프트의 reinterpret_cast 문서에서는 reinterpret_cast를 활용해 고유한 인덱스로 매핑하는 해시함수를 만들 수 있다고 설명하고 있습니다.
unsigned int 자료형에 void* 로 받아오는 value 값을 reinterpret_cast로 변환하고, 쉬프트 연산(>>)과 XOR 연산(^)을 활용해 Hash 함수를 구현할 수 있습니다. 포인터를 reinterpret_cast를 통해 정수 형식으로 처리하며 주솟값에 다양한 비트 연산을 수행하면 높은 확률로 고유한 인덱스를 생성할 수 있기 때문입니다.
이처럼, reinterpret_cast는 void*로 받아오는 value값을 어떤 특정 포인터형으로 형변환하여 사용할 때 활용될 수 있습니다.
bit_cast<> : 비트 캐스팅
C++20에서는 <bit> 헤더에 정의되어 있는 bit_cast<>()가 추가되었다. 위에 4종류의 캐스팅은 C++언어의 일부지만, bit_cast는 표준 라이브러리의 일부이다.
bit_cast는 주어진 타겟 타입의 새로운 객체를 생성하고 원본 객체를 비트로 복사한다.
bit_cast를 사용하기 위해선 효과적으로 소스와 타겟 객체의 크기가 같아야 하고, 둘다 복사 가능한 형식이어야 한다.
(코딩테스트 비트변환에서 사용해본적 있는 캐스팅이다)
번외 - 업캐스팅/다운캐스팅
부모 자식 클래스 간에 수행되는 업캐스팅과 다운캐스팅은 상속관계에서의 캐스팅 개념을 뜻하고, 실질적인 C++ 캐스팅 방식은 크게 4가지로 볼 수 있습니다.
https://junstar92.tistory.com/317
[C++] 캐스팅(Casting)
References Professional C++ https://en.cppreference.com/w/ Contents const_cast() static_cast() reinterpret_cast() dynamic_cast() std::bit_cast() C++에서는 어떤 타입을 다른 타입으로 캐스팅하기 위한, const_cast(), static_cast(), reinterp
junstar92.tistory.com
C++ 캐스팅 총정리(스마트포인터 캐스팅 포함)
캐스트는 자료형간 또는 포인터간 형변환시 사용됩니다. 캐스트는 크게 묵시적 캐스트(implicit cast)와 명시적 캐스트(explicit cast) 두 가지로 나눌 수 있습니다. 특별히 캐스트 연산자를 사용하지
ence2.github.io
⭐ 다운캐스팅 오류 예제
https://blockdmask.tistory.com/241
[C++] dynamic_cast (타입캐스트 연산자)
안녕하세요. BlockDMask 입니다. 이번에는 C++의 네가지 타입캐스트 연산자 (static_cast, const_cast, reinterpret_cast, dynamic_cast) 중에서 마지막 dynamic_cast에 대해서 알아보겠습니다. 1. dynamic_cast에 대해서 dynam
blockdmask.tistory.com
https://hwan-shell.tistory.com/219
C++] reinterpret_cast에 대해서...
모든 언어에는 형변환이 있습니다. C++에선 다양한 형번환 객체들을 제공합니다. 1. static_cast = https://hwan-shell.tistory.com/211 2. dynamic_cast = https://hwan-shell.tistory.com/213 3. const_cast = https://hwan-shell.tistory.c
hwan-shell.tistory.com
https://learn.microsoft.com/ko-kr/cpp/cpp/reinterpret-cast-operator?view=msvc-170
reinterpret_cast 연산자
자세한 정보: reinterpret_cast 연산자
learn.microsoft.com