Type Punning (타입 장난)
타입을 가지고 장난을 친다는 것이다. 어떤 타입을 나타내는 메모리 공간을 다른 타입으로 읽어서 조작한다는 개념이다. C++는 pointer 라는 개념이 있기때문에 타입 퍼닝이 쉽게 가능하다.
흔한 타입 퍼닝 예를 보자.
Example Code
struct S
{
int a;
double b;
float c;
};
void fn(unsigned char* addr, std::size_t length)
{
// send object or set object
}
int main()
{
S s;
fn((unsigned char*)&s, sizeof(s));
}
구조체 S를 만들고 이를 unsigned char*로 읽는 예제이다.
위와 같은 예제 코드를 그림으로 그려보면,
fn 함수 안에서는 S라는 오브젝트가 존재하는 주소 공간(0x1234)과 오브젝트 사이즈인 24를 함수 argument로 받는다. 이 정보를 가지고 패킷을 전송한다든지 데이터를 복사한다든지 또는 값을 넣어준다든지 하는 작업들을 수행할 수 있을 것이다.
더 간단한 예제를 보자.
Example Code
bool isNeg(float x)
{
return x < 0.0f;
}
int main()
{
std::cout << isNeg(-1.1f) << std::endl;
}
float 값이 음수인지 아닌지 체크하는 함수가 있다. 그런데 만약 현재 가진 컴퓨터의 CPU가 float 타입에 대해서 comparison instruction이 매우 느리다고 가정을 해본다. 그렇다면 위의 코드는 아주 느리게 동작할 것이다.
bool isNeg(float x)
{
unsigned int* ui = (unsigned int*)&x;
return *ui & 0x80000000;
}
이를 unsigned int로 캐스팅을 한 후에 & 연산을 한 후 리턴해줄 수 있다. 그러면 이와 같은 연산에는 float 타입의 비교가 들어가지 않으므로 느려지지 않는다. float 부동소수점에서 가장 첫번째 부호 비트와 & 연산 비교해서 음수를 체크하였다. 결국 x의 주소는 32비트 float 타입의 주소 였는데 이를 unsigned int 주소로 캐스팅을 한 것이다. 타입 float을 마치 unsigned int처럼 읽어서 장난을 친 것이다.
Advanced한 내용
타입 퍼닝을 실제로 코드에 쓰기 위해서는 다음 내용을 알아야한다.
unsigned int* ui = (unsigned int*)&x;
우선 이 코드는 타입 퍼닝을 사용하여 동작한다. 하지만 현재 내 PC 환경에서는 동작되었지만 다른 환경에서는 동작하지 않을 수 있다. 그 이유는 이 행위 자체가 Undefined 되어있기 때문이다.
포인터 캐스팅을 위해서는 아래와 같은 3가지를 제외하고는 정의되어있지 않다.
- unsigned char*
- char*
- std::byte*
즉, float를 unsigned int*의 캐스팅은 정의되어있지가 않다. 이 말은 다른 환경에서는 정상적으로 동작되지 않을 수도 있다는 의미이다.
예를 들어 union이 있다고했을 때
union U
{
float a;
unsigned int b;
};
union안에 float과 unsigned int를 정의했다. float에 값을 넣어주고 타입 퍼닝을 이용해서 unsigned int를 읽을 수 있는데 이 방식의 동작 또한 undefined behavior이기 때문에 사용하면 안된다.
올바른 사용 방식
bool isNeg(float x)
{
unsigned int tmp;
std::memcpy(&tmp, &x, sizeof(x));
return tmp & 0x80000000;
}
표준 방식으로는 값을 복사해주고 & 연산을 하게되면 정상적으로 동작하게 된다. 그런데 여기서 쓸데없는 메모리 카피를 했기 때문에 비효율적인건 아닐까라고 생각할 수 있지만 컴파일러는 똑똑하기 때문에 바이너리 코드에서는 memcpy 없이 타입 퍼닝으로 코드가 컴파일된다. 위 방식이 제일 깔끔한 방식이긴 하나 memcpy는 가독성이 떨어지는 문제가 있다.
std::bit_cast(C++20)
타입 퍼닝은 꽤나 유용하게 사용되기 때문에 이를 해결하기 위해서 C++20 부터 std::bit_cast라는 함수가 새로 추가되었다. Cppreference를 보면 C++20 이전 버전에서도 사용할 수 있도록 구현해놓은 코드도 있으므로 복사해서 타입 퍼닝을 만들 수도 있다.
bool isNeg(float x)
{
// C++20
return std::bit_cast<unsigned int>(x) & 0x80000000;
}
더 안전하게 타입 퍼닝을 사용할 수 있다.
결론적으로 C++20 이전 버전이라면 memcpy를 사용하고 C++20 이상이라면 bit_cast를 사용하면 되겠다.
'C++ > Modern C++' 카테고리의 다른 글
[C++17] std::variant (0) | 2022.11.07 |
---|---|
[C++] Union (0) | 2022.11.04 |
[C++17] std::optional (0) | 2022.10.31 |
[C++] 가상함수 원리 (0) | 2022.10.24 |
[C++] attributes(속성) (0) | 2022.10.13 |