본문 바로가기
프로그래밍/C & C++

[C++] 캐스팅(Casting)

by 별준 2022. 2. 17.

References

Contents

  • const_cast()
  • static_cast()
  • reinterpret_cast()
  • dynamic_cast()
  • std::bit_cast()

C++에서는 어떤 타입을 다른 타입으로 캐스팅하기 위한, const_cast(), static_cast(), reinterpret_cast(), dynamic_cast() 와 C++20에서부터 지원하는 std::bit_cast() 라는 5가지의 캐스팅 방법을 제공합니다.

 

(int)myFloat와 같은 C 스타일의 캐스팅도 C++에서 계속 지원하고 있으며 현재까지도 여러 코드에서 많이 사용하고 있습니다. C 스타일 캐스팅 방법은 C++의 캐스팅 기능을 모두 포함하지만 의도가 분명히 드러나지 않아서 에러가 발생하거나 예상과 다른 결과가 나올 수 있습니다. 따라서 C++ 코드를 작성한다면 C++ 스타일로 캐스팅하는 것이 좋습니다. C++ 캐스팅은 C 스타일보다 훨씬 안전할 뿐만 아니라 문법도 훨씬 깔끔합니다.

 

이번 포스팅에서는 C++의 캐스팅 방법들에 대해 알아보도록 하겠습니다.

 


const_cast()

const_cast()는 캐스팅 방법들 중에 가장 이해하기 쉬운 캐스팅 방법입니다. const_cast()는 변수에 const 속성을 추가하거나 제거할 때 사용합니다. 사실 기본 정의를 생각해보면 const 캐스팅을 할 일이 없어야 합니다. 변수를 const로 지정했다는 것은 const 상태를 유지하겠다는 뜻이기 때문입니다. 하지만 실전에서 함수를 작성할 때는 const 변수를 인수로 받도록 정의했는데, 사용하다 보니 non-const 변수도 받아야할 때가 생길 수 있습니다. 정석대로 처리하려면 프로그램 전체에서 const로 지정한 부분을 항상 일관성 있게 유지해야 하지만 서드파티 라이브러리와 같이 마음대로 수정할 수 없을 때는 부득이 const 속성을 일시적으로 제거할 수 밖에 없습니다. 단, 호출할 함수가 객체를 변겨아지 않는다고 보장될 때만 이렇게 처리해야 합니다. 그렇지 않으면 작성한 프로그램의 구조를 변경해야 합니다.

 

예를 들면 다음과 같습니다.

void ThirdPartyLibraryMethod(char* str);

void f(const char* char)
{
    ThirdPartyLibraryMethod(const_cast<char*>(str));
}

 

추가로, C++17부터 <utility> 헤더에 정의된 std::as_const()라는 헬퍼 메소드가 추가되었습니다. 이 함수는 레퍼런스 매개변수를 const 레퍼런스 버전으로 변환해줍니다. 기본적으로 as_const(obj)는 const_cast<const T&>(obj)와 같습니다. 여기서 T는 obj의 타입입니다. 아래 코드에서 볼 수 있듯이 non-const를 const로 캐스팅할 때는 const_cast()보다 as_const()를 사용하는 것이 훨씬 더 간결합니다.

std::string str{ "C++" };
const std::string& constStr{ std::as_const(str) };

 

as_const()와 auto를 조합하여 사용할 때는 주의할 점이 있습니다. auto는 레퍼런스와 const 속성을 제거하는 특징을 가지고 있는데, 따라서 다음과 같이 작성하면 result 변수의 타입은 const std::string&이 아닌 std::string이 됩니다.

std::string str{ "C++" };
auto result{ std::as_const(str) };

 


static_cast()

static_cast()는 언어에서 지원하는 명시적 변환을 수행합니다. 예를 들어 다음 코드처럼 정수에 대한 나눗셈이 아닌 부동소수점에 대한 나눗셈으로 처리하도록 int를 double로 변환해야 할 때가 있습니다. 이때 static_cast()를 사용하면 됩니다.

int i{ 3 };
int j{ 4 };
double result{ static_cast<double>(i) / j };

참고로 위 코드에서는 i에 대해서만 static_cast()를 적요해도 됩니다. C++은 피연산자 중 하나가 double이면 부동소수점 나눗셈을 적용하기 때문입니다.

 

사용자 정의 생성자나 변환 루틴(conversion routines)에서 허용하는 명시적 변환을 수행할 때도 static_cast()를 사용할 수 있습니다. 예를 들어 A 클래스의 생성자 중에 B 클래스 객체를 인수로 받는 버전이 있다면 B 객체를 A 객체로 변환하는데 static_cast()를 이용할 수 있습니다. 그런데 이런 변환은 대부분 컴파일러가 자동으로 처리해줍니다.

 

상속 계층에서 다운캐스팅을 수행할 때도 static_cast()를 사용할 수 있습니다.

예를 들면 다음과 같습니다.

class Base
{
public:
    virtual ~Base() = default;
};

class Derived : public Base
{
public:
    virtual ~Derived() = default;
};

int main()
{
    Base* b{ nullptr };
    Derived* d{ new Derived{} };
    b = d; // Don't need a cast to go up the inheritance hierarchy
    d = static_cast<Derived*>(b); // Need a cast to go down the hierarchy

    Base base;
    Derived derived;
    Base& br{ derived };
    Derived& dr{ static_cast<Derived&>(br) };
}

이러한 캐스팅은 포인터나 레퍼런스에도 적용할 수 있습니다만, 객체 자체에는 적용할 수 없습니다.

 

이렇게 static_cast()를 사용할 때는 실행 시간에 타입 검사를 수행하지 않는다는 점에 주목할 필요가 있습니다. 실행 시간에 캐스팅할 때는 Base와 Derived가 실제로 관련이 없어도 Base 포인터나 레퍼런스를 모두 Derived 포인터나 레퍼런스로 변경합니다. 예를 들어 다음과 같이 작성하면 컴파일 과정이나 실행 과정에서는 아무런 문제가 발생하지 않지만 포인터 d를 사용하다가 객체의 범위를 벗어난 영역의 메모리를 덮어쓰는 심각한 문제가 발생할 수 있습니다.

Baes* b{ new Base{} };
Derived* d{ static_cast<Derived*>(b) };

실행 시간에 타입을 검사하여 타입을 안전하게 캐스팅하려면 dynamic_cast()를 사용해야 하는데, 이는 아래에서 살펴보도록 하겠습니다.

 

static_cast()는 그리 강력한 기능은 아닙니다. 포인터의 타입이 서로 관련이 없을 때는 static_cast()를 적용할 수 없습니다. 또한 변환 생성자가 제공되지 않는 타입의 객체에도 static_cast()를 적용할 수 없습니다. const 타입을 non-const 타입으로 변환할 수도 없고, int에 대한 포인터에도 적용할 수 없습니다. 기본적으로 C++의 타입 규칙에서 허용하지 않는 것은 모두 할 수 없다고 보면 됩니다.

 


reinterpret_cast()

reinterpret_cast()는 static_cast()보다 더 강력하지만 안전성은 조금 떨어집니다. reinterpret_cast()는 C++ 타입 규칙에서 허용하지 않더라도 상황에 따라 캐스팅하는 것이 적합할 때 사용할 수 있습니다. 예를 들어 서로 관련이 없는 레퍼런스끼리 변환할 수도 있습니다. 마찬가지로 상속 계층에서 아무런 관련이 없는 포인터 타입끼리도 변환할 수 있습니다. 이런 포인터는 흔히 void* 타입으로 캐스팅합니다. 이 작업은 내부적으로 처리되기 때문에 명시적으로 캐스팅하지 않아도 됩니다. 하지만 이렇게 void*로 변환한 것을 다시 원래 타입으로 캐스팅할 때는 reinterpret_cast()를 사용해야 합니다. void* 포인터는 메모리의 특정 지점을 가리키는 포인터일 뿐 void* 포인터 자체에는 아무런 타입 정보가 없기 때문입니다.

 

예를 들면 다음과 같습니다.

class X {};
class Y {};

int main()
{
    X x;
    Y y;
    X* xp{ &x };
    Y* yp{ &y };
    // Need reinterpret_cast for pointer conversion from unrelated classes
    // static_cast doesn't work
    xp = reinterpret_cast<X*>(yp);
    // No cast required for conversion from pointer to void*
    void* p{ xp };
    // Need reinterpret_cast for pointer conversion from void*
    xp = reinterpret_cast<X*>(p);
    // Need reinterpret_cast for reference conversion from unrelated classes
    // static_cast doen't work
    X& xr{ x };
    Y& yr{ reinterpret_cast<Y&>(x) };
}

 

reinterpret_cast()가 모든 것에 강력한 것은 아닙니다. 무엇에 캐스팅될 수 있는지에 대한 제약이 있는데, 이에 대해선 크게 다루지는 않겠습니다. 다만 reinterpret_cast()를 사용할 때 주의해서 사용해야 하는데, 이는 reinterpret_cast()가 타입 검사를 하지 않고 변환할 수 있기 때문입니다.

 

포인터를 정수형 타입으로 변환하거나 그 반대로 변환할 때도 reinterpret_cast()를 사용할 수 있습니다. 단 이때 정수형의 크기가 포인터를 담을 정도로 충분히 커야 합니다. 예를 들어 64비트 포인터를 32비트 int로 변환하는 작업을 reinterpret_cast()로 처리하면 컴파일 에러가 발생합니다.

 


dynamic_cast()

dynamic_cast()는 같은 상속 계층에 속한 타입끼리 캐스팅할 때 실행 시간에 타입을 검사합니다. 포인터나 레퍼런스를 캐스팅할 때 이를 이용할 수 있습니다. dynamic_cast()는 내부 객체의 타입 정보를 실행 시간에 검사합니다. 그래서 캐스팅하는 것이 적합하지 않다고 판단하면 포인터에 대해서는 널 포인터를 리턴하고 레퍼런스에 대해서는 std::bad_cast 예외를 발생시킵니다.

 

예를 들어, 다음과 같이 클래스 계층이 구성된 경우를 살펴보겠습니다.

class Base
{
public:
    virtual ~Base() = default;
};

class Derived : public Base
{
public:
    virtual ~Derived() = default;
};

 

이때 dynamic_cast()의 올바른 사용 예시는 다음과 같습니다.

int main()
{
    Base* b;
    Derived* d{ new Derived{} };
    b = d;
    d = dynamic_cast<Derived*>(b);
}

 

반면 레퍼런스에 대해 다음과 같이 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()는 실행 시간에 타입 검사를 수행하는 반면 static_cast()나 reinterpret_cast()는 문제가 되는 타입도 그냥 캐스팅해버린다는 것입니다.

 

실행 시간의 타입 정보는 객체의 vtable에 저장됩니다. 따라서 dynamic_cast()를 적용하려면 클래스에 virtual 메소드가 최소한 한 개 이상 있어야 합니다. 그렇지 않은 객체에 대해 dynamic_cast()를 적용하면 컴파일 에러가 발생합니다.

예를 들어, Microsoft Visual C++의 경우에는 다음의 에러가 발생합니다.

 


std::bit_cast()

C++20에서는 <bit> 헤더에 정의되어 있는 std::bit_cast()가 추가되었습니다. 이 캐스트는 표준 라이브러리의 일부이며, 위에서 설명한 다른 캐스트들은 C++ 언어의 일부입니다. bit_cast()는 reinterpret_cast()와 비슷하지만, bit_cast()는 주어진 타겟 타입의 새로운 객체를 생성하고 원본 객체를 비트로 복사합니다. bit_cast()는 효과적으로 소스 객체를 비트로 해석합니다. bit_cast()를 사용할 때는 소스와 타겟 객체의 크기가 같아야 하고, 둘다 복사 가능한 형식(trivially copyable)이어야 합니다.

 

예를 들면 다음과 같습니다.

int main()
{
    float asFloat{ 1.23f };
    auto asUint{ std::bit_cast<unsigned int>(asFloat) };
    if (std::bit_cast<float>(asUint) == asFloat)
        std::cout << "Foundtrip success." << std::endl;
}

 

bit_cast()는 단순 복사 가능 타입(trivially copyable types)에 대해 바이너리 I/O를 수행하는 경우에 사용될 수 있습니다. 예를 들어, 이런 타입의 값을 파일에 바이트로 썻다가 나중에 다시 파일을 읽어서 메모리로 불러올 때 bit_cast()를 적용하면 다시 원래 값 그대로 정확히 해석할 수 있습니다.

trivially copyable type이란 객체를 구성하는 내부 바이트를 char와 같은 타입의 배열처럼 비트 단위 복사로 쉽게 변환할 수 있는 타입을 말합니다. 이렇게 복사해둔 배열의 데이터는 나중에 다시 객체로 복사하면 원래 값을 그대로 유지합니다.

 


Summary

 

댓글