캐스트(Cast) — 명시적 형변환

캐스트(Cast) — 명시적 형변환

값의 타입을 변환할 때, 컴파일러가 알아서 처리하는 암묵적 변환과 달리 우리가 직접 타입을 지정해서 바꾸는 방법이 있어요. 그것이 **캐스트(cast)**예요. (double)x처럼 괄호 안에 타입 이름을 적고 표현식을 붙이는 형태로, "이 값을 이 타입으로 바꿔서 다뤄라"고 명시하는 거예요. 주의할 점은 캐스트가 허용하는 변환의 목록이 제한적이라는 거예요. (type-name) expression 형태의 문법과 허용되는 변환 규칙을 살펴볼게요.

출처: cppreference

본문

명시적 타입 변환(explicit type conversion)을 수행해요.

문법(Syntax)

( type-name ) expression
  • type-namevoid 타입이거나 어떤 스칼라 타입(scalar type).
  • expression — (type-name이 void가 아니라면) 스칼라 타입의 어떤 표현식. void라면 무엇이든 될 수 있어요.

설명(Explanation)

type-name이 void라면, 표현식은 부수 효과를 위해 평가되고 반환값은 버려져요. 표현식 문으로 표현식 자체를 그냥 쓴 것과 같아요.

그 외에 type-name이 정확히 표현식의 타입과 같으면 아무 일도 하지 않아요. 단, 표현식이 부동 타입이고 그 타입이 나타내는 것보다 더 큰 범위와 정밀도로 표현되는 경우(아래 참고)는 예외예요.

그 외에는 표현식의 값이 type-name이 가리키는 타입으로 다음과 같이 변환돼요.

할당에 의한 것처럼 모든 암묵적 변환이 허용돼요. 암묵적 변환에 더해 다음 변환들도 허용돼요.

  • 어떤 정수도 어떤 포인터 타입으로 캐스트할 수 있어요. NULL 같은 널 포인터 상수(캐스트가 필요 없어요)를 제외하면, 결과는 구현 정의이고, 올바르게 정렬되지 않을 수 있고, 참조된 타입의 객체를 가리키지 않을 수 있으며, 트랩 표현일 수 있어요.
  • 어떤 포인터 타입도 어떤 정수 타입으로 캐스트할 수 있어요. 널 포인터 값도 결과는 구현 정의예요(반드시 0이 되지는 않아요). 결과를 대상 타입에 표현할 수 없으면 동작은 미정의예요(부호 없는 정수는 포인터에서의 캐스트에 대해 모듈러 산술을 구현하지 않아요).
  • 객체에 대한 어떤 포인터도 객체에 대한 다른 어떤 포인터로 캐스트할 수 있어요. 값이 대상 타입에 대해 올바르게 정렬되지 않으면 동작은 미정의예요. 그 외에는 값을 원래 타입으로 다시 변환하면 원래 값과 같게 비교돼요. 객체 포인터를 어떤 문자 타입의 포인터로 캐스트하면, 결과는 객체의 가장 낮은 바이트를 가리키고 대상 타입의 sizeof까지 증가시킬 수 있어요. 즉 객체 표현을 조사하거나 memcpy/memmove로 복사본을 만드는 데 쓸 수 있어요.
  • 함수에 대한 어떤 포인터도 다른 어떤 함수 타입의 포인터로 캐스트할 수 있어요. 결과 포인터를 원래 타입으로 되돌리면 원래 값과 같게 비교돼요. 변환된 포인터로 함수 호출을 하면 (함수 타입이 호환되지 않는 한) 동작은 미정의예요.
  • 포인터들 사이에서 캐스트할 때(객체든 함수든), 원래 값이 자기 타입의 널 포인터 값이면 결과는 대상 타입에 대한 올바른 널 포인터 값이 돼요.

어느 경우든(암묵적 변환을 실행할 때와 같은 타입 캐스트에서 모두), 표현식과 type-name이 부동 타입이고 표현식이 그 타입이 나타내는 것보다 더 큰 범위와 정밀도로 표현된다면(FLT_EVAL_METHOD 참고), 대상 타입에 맞게 범위와 정밀도가 벗겨져요.

캐스트 표현식의 값 범주(value category)는 항상 비-좌측값(non-lvalue)이에요.

참고(Notes)

const, volatile, restrict, _Atomic 지정자는 lvalue에만 영향이 있으므로, cvr-한정 또는 원자 타입으로의 캐스트는 대응하는 비한정 타입으로의 캐스트와 정확히 동일해요.

void로의 캐스트는 사용하지 않는 결과에 대한 컴파일러 경고를 조용히 만들 때 유용할 수 있어요.

여기 나열되지 않은 변환은 허용되지 않아요. 특히,

  • 포인터와 부동 타입 사이의 변환은 없어요.
  • 함수 포인터와 객체 포인터( void* 포함) 사이의 변환은 없어요.

구현이 intptr_t/uintptr_t를 제공한다면, 객체 타입( cv void 포함)에 대한 포인터에서 이 타입들로의 캐스트는 항상 잘 정의돼요 (C99부터). 다만 함수 포인터에 대해서는 보장되지 않아요.

함수 포인터와 객체 포인터 사이의 변환은 많은 컴파일러가 확장으로 받아들이고, POSIX의 dlsym()과 같은 사용처에서 기대되기도 해요.

예제(Example)

#include <stdio.h>

int main(void)
{
    // examining object representation is a legitimate use of cast
    double d = 3.14;
    printf("The double %.2f (%a) is: ", d, d);
    for (size_t n = 0; n < sizeof d; ++n)
        printf("0x%02x ", ((unsigned char*)&d)[n]);

    // edge cases
    struct S { int x; } s;
//    (struct S)s; // error; not a scalar type
                   // even though casting to the same type does nothing
    (void)s; // okay to cast any type to void
}

가능한 출력:

The double 3.14 (0x1.91eb851eb851fp+1) is: 0x1f 0x85 0xeb 0x51 0xb8 0x1e 0x09 0x40

double 리터럴을 (unsigned char*)로 캐스트해서 객체 표현의 각 바이트를 찍어낼 수 있어요. 이건 캐스트의 정당한 사용처 중 하나예요. 반면 struct S처럼 스칼라가 아닌 타입으로의 캐스트는 그 타입이 같더라도 허용되지 않아요. 다만 void로는 어떤 타입이든 캐스트할 수 있어요.

더 알아보기

  • 암묵적 타입 변환에 대한 Implicit conversions 문서에서 캐스트와의 차이를 다뤄요.
  • intptr_t에 대한 세부는 <stdint.h> 문서를 참고해요.