C++ Casting
C++에는 여러가지 캐스팅 방법들이 존재한다.
C Style Casting
C에서 계승받은 전통적인 캐스팅 방법으로 여러 캐스팅 작업을 복합적으로 사용한다.
편리할 수 있지만 덜 명확하고 예상치 못한 동작이 발생할 수 있다.
잠재적인 문제를 숨길 수 있으므로 최신 C++ 코드에서는 권장되지 않는다.
double numDouble = (double)42;
최신 C++에서는 가능하면 static_cast, dynamic_cast, const_cast와 같은 보다 구체적인 형변환 연산자를 사용하는 것이 일반적으로 권장된다. 이러한 연산자는 더 나은 유형 안전성을 제공하고 코드를 보다 명확하게 구분할 수 있도록 해주기 때문이다. 그러나 하위 수준 작업에 reinterpret_cast를 사용해야 하거나 레거시 코드 또는 C 인터페이스와의 호환성을 위해 C 스타일 캐스팅을 사용해야 하는 상황이 있다. 이런 상황들은 최대한 구분된 함수에서 처리하도록 하는 것이 좋을 것이다.
Static Casting
안전하고 대부분 컴파일 타임 유형 변환에 사용된다. 숫자 변환, 포인터 변환, 사용자 정의 유형 변환에 사용할 수 있다.
유형 호환성을 보장하기 위해 컴파일 타임에 검사를 수행한다. (Run-Time 검사는 수행하지 않는다.)
컴파일 시점에서 타입 변환에 대한 무결성을 검사하며, ‘허용’ 과 ‘컴파일러에 의한 값 변환’ 이라는 두 가지 관점에서 이루어진다.
static_cast의 적절한 사용 사례
1. 숫자 변환: 'static_cast'는 데이터 손실이나 정밀도 문제의 위험이 없는 경우 숫자 데이터 유형 간 변환에 적합하다.
double numDouble = static_cast<double>(42);
2. 상속 계층 구조의 포인터 변환: 변환이 안전하다고 확신할 때 상속 계층 구조 내에서 포인터나 참조를 변환하는 데 적합하다. 즉, 아래와 같은 변환이 가능하지만 이 코드가 항상 정상동작할 것이라는 확신이 있어야 한다. (아래 부적절한 사례에서 보충 설명)
class Base {};
class Derived : public Base {};
Base* basePtr = new Derived;
Derived* derivedPtr = static_cast<Derived*>(basePtr);
3. Enum 변환: static_cast를 사용하면 enum 값을 기본 정수 유형으로 변환하거나 그 반대로 변환할 수 있다.
enum Color { Red, Green, Blue };
int colorValue = static_cast<int>(Red);
4. 사용자 정의 유형 변환: 클래스에 유형 변환 연산자나 생성자를 정의한 경우 static_cast를 사용하여 이를 명시적으로 호출할 수 있다.
class MyType {
public:
operator int() const {
return 42;
}
};
MyType myObj;
int intValue = static_cast<int>(myObj);
static_cast의 부적절한 사용 사례
1. 안전하지 않은 변환 수행: 'static_cast'는 관련되지 않은 유형 간 변환이나 데이터 손실이 예상되는 경우와 같이 안전하지 않은 유형 변환에 사용해서는 안 된다. 이러한 경우에는 reinterpret_cast를 사용하거나(주의해서) 코드 디자인을 다시 살펴보는 것이 좋다.
float floatVal = static_cast<float>(42); // Inappropriate if precision loss is not acceptable
2. const 한정자 우회: static_cast를 사용하여 변수에서 const 한정자를 제거하면 실제로 const 개체를 수정하려고 시도하는 경우 정의되지 않은 동작이 발생할 수 있다. const_cast를 명시적으로 사용하는 것이 좋다.
const int constValue = 42;
int* nonConstPtr = static_cast<int*>(&constValue); // Inappropriate
3. 런타임 검사 없이 다운캐스트: 'static_cast'를 사용하여 런타임 검사 없이 상속 계층의 포인터를 다운캐스트하면 변환이 유효하지 않은 경우 정의되지 않은 동작이 발생할 수 있다. 이러한 경우 대신 런타임 유형 검사와 함께 'dynamic_cast'를 사용하도록 한다.
class Base {};
class Derived : public Base {};
void Function(Base* basePtr)
{
Derived* derivedPtr = static_cast<Derived*>(basePtr); // Inappropriate
if(derivedPtr != nullptr)
{
// ..
}
}
위의 적절한 사례에서 봤던 똑같은 예제가 부적절한 사례에도 있다는 것이 의아할 수 있다. 위의 예제에서는 코드에서 다운 캐스팅되는 특정 객체로의 변환에 대한 확신이 있었던 반면, 아래 예제는 basePtr에 다른 파생 객체가 들어있을 수도 있다는 가정이 있기 때문이다.
ps.
상속과 Base 포인터를 활용하는 것은 대부분 파생 객체들에 대한 다형성 때문인데, 개인적인 생각으로는 왜 static_cast가 이러한 상황을 허용시킨 것인지 이해가 가지 않는다. 물론 파생객체가 어떤 특수객체임을 반드시 확신할 수 있는 상황이라면, 굳이 dynamic cast를 이용하여 runtime check 비용을 낭비하지 않아도 된다는 장점이 있지만, 별로 설득력이 있는 이유같지는 않다. 왠만하면 다형성과 관련된 상황에서의 캐스팅은 dynamic_cast를 사용하도록 하자.
Dynamic Casting
다형성 유형(가상 함수가 있는 클래스)의 맥락에서 안전한 런타임 유형 검사 및 변환에 사용된다.
일반적으로 상속 계층 작업 시 기본 클래스에 대한 포인터 또는 참조와 함께 사용된다.
런타임에 변환이 유효하지 않으면 널 포인터를 반환한다.
런타임에 vtable에 대한 탐색을 수행하기 때문에 약간의 계산 비용이 발생할 수 있다.
다음과 같이 사용된다.
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr)
{
std::cout << "Valid dynamic_cast" << std::endl;
}
else
{
std::cout << "Invalid dynamic_cast" << std::endl;
}
참고로 static_cast는 위와 같은 상황에 대응하지 못한다.
Derived* derivedPtr = static_cast<Derived*>(basePtr);
if (derivedPtr)
{
// basePtr에 어떤 객체가 들어있건 이 블럭으로 들어온다.
// 물론 Derived 객체가 아닌 다른 객체인 경우 그 값이 정상적이지 않을 것이다.
}
else
{
}
Reinterpret Casting
종종 관련되지 않은 유형 간의 낮은 수준의 안전하지 않은 유형 변환에 사용된다.
유형 검사 없이 값의 이진 표현을 변경한다.
일반적인 사용은 권장하지 않으며 주의해서 사용해야 한다.
int num = 42;
float* floatPtr = reinterpret_cast<float*>(&num);
ps.
보통 개발 초반에는 reinterpret_cast를 왜 써야 하는지 잘 모르는데 그런 경우 custom serializer를 만들어보면 좋다. 예를 들어 클라이언트에서 서버로 데이터를 줘야 하는 경우 데이터를 byte stream으로 직렬화 해야 할 것이다. byte stream은 일반적으로는 unsigned char의 배열 형태로 정의될 것이다. char와 같은 1byte의 데이터는 static_cast를 통해 unsigned char로 캐스팅이 가능하지만, int와 같은 4byte 데이터는 어떻게 해야 하는가? 이 때 reinterpret_cast를 이용하면 int를 4개의 unsigned char로 변환할 수 있다.
즉, reinterpret_cast는 개발자에게 있어 없어서는 안될 개념이다.
class Person
{
public:
std::string name;
int age;
// Serialize the object to a binary stream
void Serialize(unsigned char* buffer) const
{
// Copy the name as a null-terminated string
std::strcpy(reinterpret_cast<char*>(buffer), name.c_str());
// Copy the age as an integer
std::memcpy(buffer + name.size() + 1, &age, sizeof(age));
}
// Deserialize a binary stream to create an object
static Person Deserialize(const unsigned char* buffer)
{
Person person;
// Read the name as a null-terminated string
person.name = reinterpret_cast<const char*>(buffer);
// Read the age as an integer
std::memcpy(&person.age, buffer + person.name.size() + 1, sizeof(person.age));
return person;
}
};
Const Casting
변수에서 const 한정자를 추가하거나 제거하는 데 사용된다.
const 및 non-const 오버로드가 모두 있는 함수로 작업할 때 유용하다.
const 객체의 수정을 활성화하거나 const가 아닌 객체를 상수로 만드는 데 사용할 수 있다.
기타
https://stackoverflow.com/questions/4167304/why-should-casting-be-avoided
'프로그래밍 > C++' 카테고리의 다른 글
[C++] C++에서 Partial Class 구현 방법 (0) | 2023.09.23 |
---|---|
[C++] C++에서 Interface 구현 방법 (0) | 2023.09.23 |
[C++] 객체 복사 (0) | 2021.04.27 |
[C++] C++의 데이터 타입 종류 (0) | 2021.04.17 |
[C++] friend (2) | 2021.04.06 |