C++ casting

5 분 소요

casting

C++는 const_cast(), static_cast(), reinterpret_cast(), dynamic_cast()라는 네가지 캐스팅 방법을 지원한다. ()을 이용하는 C스타일 캐스팅도 C++에서 계속 지원하고 있으며 현재까지도 여러 C++ 프로젝트에서 많이 사용하고 있다.
C 스타일 캐스팅 방법은 위 4가지 모두를 포함하지만 의도가 분명히 드러나지 않아서 에러가 발생하거나 예상과 다른 결과가 나올 수 있다. 따라서 C++코드를 처음부터 새로 작성할 때는 반드시 C++ 스타일로 캐스팅 하는 것이 좋다. C 스타일보다 훨씬 안전할 뿐만 아니라 문법도 훨씬 깔끔하기 때문이다.

usage

  • const 속성 제거: const_cast()
  • 언어에서 허용하는 명시적 변환 (int -> double): static_cast()
  • 사용자 정의 생성자나 변환 연산자에서 지원하는 명시적 변환: static_cast()
  • 서로 관련 없는 타입의 객체끼리 변환: 불가능
  • 같은 상속 계층에 있는 클래스 타입의 객체 포인터 사이의 변환: dynamic_cast() 권장, static_cast()도 가능
  • 같은 상속 계층에 있는 클래스 타입의 객체 레퍼런스 사이의 변환: dynamic_cast() 권장, static_cast()도 가능
  • 서로 관련 없는 타입의 포인터 사이의 변환: reinterpret_cast()
  • 서로 관련 없는 타입의 레퍼런스 사이의 변환: reinterpret_cast()
  • 함수 포인터 사이의 변환: reinterpret_cast()

const_cast()

const_cast()는 네 가지 캐스팅 방법 중에서 가장 이해하기 쉽다. 변수에 const 속성을 추가하거나 제거할 때 사용한다. 네 가지 캐스팅 방법 중 const_cast()만 const속성을 제거하는 기능을 제공한다.

코드를 작성하다 보면 non-const 변수도 받아야 할 때가 생길 수 있다. 정석대로 처리하려면 프로그램 전체에서 const로 지정한 부분을 항상 일관성 있게 유지해야 하지만 서드파티 라이브러리와 같이 마음대로 수정할 수 없을 때는 부득이 const 속성을 일시적으로 제거할 수밖에 없다.

단, 호출할 함수가 객체를 변경하지 않는다고 보장될 때만 이렇게 처리를 해야 한다.
예를 들면 다음과 같다.
extern void ThirdPartyLibraryMethod(char* str);

void foo(const char* str)
{
    ThirdPartyLibraryMethod(const_cast<char*>(str));
}
NOTE_ C++17부터 std::as_const()란 헬퍼 메서드가 추가됐다. 이 메서드는 <utility> 헤더에 정의돼 있으며, 레퍼런스 매개변수를 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);

NOTE_ as_const()와 auto를 조합할 때 주의할 점이 있다. 1장에서 설명했듯이 auto는 레퍼런스와 const속성을 제거한다. 따라서 다음과 같이 작성하면 result 변수의 타입은 const std::string&가 아닌 std::string이 된다.
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;

사용자 정의 생성자나 변환 루틴에서 허용하는 명시적 변환을 수행할 때도 static_cast()를 사용할 수 있다. 예를 들어 A 클래스의 생성자 중에 B 클래스 객체를 인수로 받는 버전이 있다면 B 객체를 A 객체로 변환하는데 static_cast()를 이용할 수 있다. 그런데 이런 변환은 대부분 컴파일러가 알아서 처리해준다. 상속 계층에서 하위 타입으로 다운캐스팅 할 때도 static_cast()를 사용한다.
예를 들면
#include <Printer.h>
#include <iostream>
#include <sstream>

class Base
{
  public:
    Base(int line)
    {
        utilties::Printer(utilties::LogLevel::DEBUG).log(__PRETTY_FUNCTION__, std::stringstream()
            << "Base: " << line);
    }
    virtual ~Base() = default;
};
class Derived : public Base
{
  public:
    Derived(int line) : Base(line)
    {
        utilties::Printer(utilties::LogLevel::DEBUG).log(__PRETTY_FUNCTION__, std::stringstream()
            << "Derived: " << line);
    }
    virtual ~Derived() = default;
};

int main(int argc, char *argv[])
{
    Base *b;
    Derived *d = new Derived(__LINE__);
    b = d;                         // 상속 계층의 상위 타입으로 업캐스팅할 필요 없다.
    d = static_cast<Derived *>(b); // 상속 계층의 하위 타입으로 다운캐스팅해야 한다.

    Base base(__LINE__);
    Derived derived(__LINE__);
    Base &br = derived;
    Derived &dr = static_cast<Derived &>(br);
    return 0;
}d
결과는 아래와 같다.
[DEBUG], From: [Base::Base(int)], Message Base: 38 [DEBUG], From: [Derived::Derived(int)], Message Derived: 38 [DEBUG], From: [Base::Base(int)], Message Base: 42 [DEBUG], From: [Base::Base(int)], Message Base: 43 [DEBUG], From: [Derived::Derived(int)], Message Derived: 43

이러한 캐스팅은 포인터나 레퍼런스에도 적용할 수 있다. 단 객체 자체에는 적용할 수 없다. static_cast()을 사용할 때는 실행 시간에 타입 검사를 수행하지 않는다는 점에 주의한다. 실행 시간에 캐스팅할 때는 Base와 Derived가 실제로 관련이 없어도 Base 포인터나 레퍼런스를 모드 Drived 포인터나 레퍼런스로 변경한다. 예를 들어다음과 같이 작성하면 컴파일 과정과 실행 과정에 아무런 문제가 발생하지 않지만 포인터 d를 사용하다가 객체의 범위를 벗어난 영역의 메모리를 덮어쓰는 심각한 문제가 발생할 수 있다.
Base* b = new Base();
Derived* d = static_cast<Derived*>(b);

NOTE_ 타입을 안전하게 캐스팅하도록 실행 시간에 타입 검사를 적용하려면 dynamic_cast()를 사용한다.

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

reinterpret_cast()

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

int main(int argc, char *argv[])
{
    X x;
    Y y;
    X *xp = &x;
    Y *yp = &y;
    //  서로 관련 없는 클래스 타입의 포인터를 변환할 때는 reinterpret_cast()를 써야 한다.
    //  static_cast()는 작동하지 않는다.
    xp = reinterpret_cast<X*>(yp);
    //  포인터를 void*로 변환할 때는 캐스팅하지 않아도 된다.
    void* p = xp;
    //  변환된 void*를 다시 원래 포인터로 복원할 때는 reinterpret_cast()를 써야 한다.
    xp = reinterpret_cast<X*>(p);
    //  서로 관련 없는 클래스 타입의 레퍼런스를 변환할 때는 reinterpret_cast()를 써야 한다.
    //  static_cast()는 작동하지 않는다.
    X& xr = x;
    Y& yr = reinterpret_cast<Y&>(x);
    return 0;
}
reinterpret_cast()를 활용하는 예로 '단순 복사 기능 타입'에 대해 바이너리 I/O를 수행하는 경우를 들 수 있다. 예를 들어 이런 타입의 값을 파일에 바이트로 썼다가 나중에 다시 파일을 읽어서 메모리로 불러올 때 reinterpret_cast()를 적용하면 값 그대로 정확히 해석할 수 있다.
NOTE_ 단순 복사 기능 타입(trivially copyable type)이란 객체를 구성하는 내부 바이트를 char와 같은 타입의 배열처럼 비트 단위 복사로 쉽게 변환할 수 있는 타입을 말한다. 이렇게 복사해둔 배열의 데이터는 나중에 다시 객체로 복사하면 원래 값을 그대로 유지한다.
NOTE_ 포인터를 int타입으로 변환하거나 그 반대로 변환할 때도 reinterpret_cast()를 사용할 수 있다. 단 이때 int의 크기가 포인터를 담을 정도로 충분히 커야 한다. 예를 들어 64비트 포인터를 32비트 int로 변환하는 작업을 reinterpret_cast()로 처리하면 컴파일 에러가 발생한다.

dynamic_cast()

dynamic_cast()는 같은 상속 계층에 속한 타입끼리 캐스팅할 때 실행 시간에 타입을 검사한다. 포인터나 레퍼런스를 캐스팅할 때 이럴 활용할 수 있다. dynamic_cast()는 내부 객체의 타입 정보를 실행 시간에 검사한다. 그래서 캐스팅하는 것이 적합하지 않다고 판단하면 포인터에 대해서는 널 포인터를 리턴하고, 레퍼런스에 대해서는 std::bad_cast 익셉션을 발생시킨다.
#include "Printer.h"
#include <iostream>
#include <sstream>

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

int main(int argc, char *argv[])
{
    //  올바른 사용법
    Base *b;
    Derived *d = new Derived();
    b = d;
    d = dynamic_cast<Derived *>(b);
    // 레퍼런스에 대해 다음과 같이 적용하면 익셉션이 발생한다.
    Base base;
    Derived derived;
    Base &br = base;
    try
    {
        Derived &dr = dynamic_cast<Derived &>(br);
    }
    catch (const std::exception &e)
    {
        utilties::Printer(utilties::LogLevel::ERROR).log(__PRETTY_FUNCTION__, std::stringstream() 
            << e.what());
    }
}
static_cast()나 reinterpret_cast()로도 같은 상속 계층의 하위 타입으로 캐스팅할 수 있다. 차이점은 dynamic_cast()는 실행 시간에 타입 검사를 수행하는 반면 static_cast()나 reinterpret_cast()는 문제가 되는 타입도 그냥 캐스팅해버린다는 것이다.
실행 시간의 타입 정보는 객체의 vtable에 저장된다. 따라서 dynamic_cast()를 적용하려면 클래스에 virtual메서드가 최소한 한 개 이상 있어야 한다. 그렇지 않은 객체에 대해 dynamic_cast()를 적용하면 컴파일 에러가 발생한다.

참고

태그:

카테고리:

업데이트: