[C++] 캐스팅에 대해서

조재훈·2024년 2월 2일

캐스팅

C++에서의 캐스팅(형 변환)은 다른 언어와 마찬가지로 하나의 자료형을 다른 자료형으로 변환하는 것을 의미한다.
예를 들어 double형 변수를 int형 변수에 담는 아주 기초적인 예시가 있다.

C 스타일 캐스팅

C 언어에서의 형 변환은 아주 익숙한 코드들이 보일 것이다

int main()
{
	int a = 3;
    double b = 3.5414;
    
    int c = b;	// 묵시적 캐스트
    int d = (int)b;	// 명시적 캐스트
}

컴파일러가 알아서 형 변환을 해주는 묵시적 캐스트와 프로그래머가 변환할 자료형을 지정해주는 명시적 캐스트가 있다

다음의 C 스타일 캐스팅은 여전히 C++에서도 쓰이기는 하지만 C++에는 캐스팅 방법이 따로 있다.

  • static_cast
  • const_cast
  • reinterpret_cast
  • dynamic_cast
  • bit_cast (C++20에서 부터 지원)

C++에서 C 스타일의 캐스팅은 뒤에서 설명할 C++의 캐스트 방법 중 하나를 선택해 진행되는데 프로그래머 입장에서는 어떤 형 변환 방법이 선택되었는지 알 수 없어(컴파일러의 의도를 모름) 명백한 실수를 컴파일러가 캐치하지 못해 위험한 상황이 발생할 수 있다.
그러므로 C++ 개발자라면 C 스타일의 캐스팅을 되도록 쓰지 않고 안전하고 깔끔한 C++ 캐스팅을 사용하자!

static_cast

정적 캐스트, 정적이라고 하면 언어에서 컴파일시에~ / 동적이라고 하면 런타임시에~를 의미한다

즉 static_cast 연산자를 통해 형변환을 하면 컴파일(정적) 타임에 형 변환이 가능한지(오류를 체크)를 검사한다
기본 자료형간의 형변환도 허용하며 포인터 타입의 형 변환은 허용하지 않지만 상속 관계에 있는 클래스 타입의 형 변환은 허용한다.

이때 상속 관계에서 static_cast를 사용할 때 주의할 점이 있다.

class Base
{
}

class Derived : public Base
{
}

업캐스팅

업캐스팅은 자식 클래스의 참조나 포인터를 부모 클래스로 변환하는 것을 의미한다

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

자식 클래스는 부모 클래스의 멤버 변수나 함수를 포함하기 때문에 업캐스팅은 항상 안전하며, 컴파일러가 변환을 암묵적으로 처리하기에 명시적으로 적어줄 필요가 없다

다운캐스팅

다운캐스팅은 부모 클래스의 참조나 포인터를 자식 클래스로 변환하는 것을 의미한다

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

반대로 부모 클래스는 자식 클래스에 추가된 멤버 변수나 함수에 대한 정보가 없기 때문에 다운캐스팅은 안전하지 않으며, Crash가 날 수 있다.
그래서 다운캐스팅 시에는 뒤에서 볼 dynamic_cast를 사용한다.

정리

static_cast는 형변환에 대한 타입체크를 런타임에 하지 않고 컴파일 타임에 정적으로 수행한다
static_cast는 일반적으로 다른 연산자들과 달리 기본 자료형의 형변환을 허용하므로 형 변환 연산자 중 가장 사용 빈도가 높다
그리고 다운캐스팅과 같은 오류를 체크 안하므로 성능은 더 좋을 수도 있다

const_cast

이 형변환 연산자는 쉽게 말해 변수에 const 속성을 추가하거나 제거할 때 사용하는 연산자이다

  • 사실 const 캐스팅을 할 일이 없어야 하는데(변수를 안 건들이겠다고 선언했으므로) 실전에서 어쩌다 보니 사용해야 할 때가 있다.
    • 예를 들어 const 변수를 인수로 받는 함수를 작성하려고 했는데 사용하다 보니 비const 변수도 받아야할 때가 생기는 경우가 있음. 라이브러리 함수 같이 마음대로 수정할 수 없는 함수를 호출해야 할 때와 같이
  • 이 때 부득이하게 const 속성을 일시적으로 제거할 때 사용하는 연산자이다.
    • 호출할 함수에서 객체를 수정하지 않는다고 보장할 때만 이렇게 처리해야 함.
void LibraryFunc(char* str);

void func(const char* str)
{
	LibraryFunc(const_char<char*>(str));
}

reinterpret_cast

어떠한 포인터 타입도 어떠한 포인터 타입으로 변환이 가능한 연산자

  • static_cast보다 더 강력하지만 안전성은 조금 떨어짐
  • C++ 타입 규칙에서 허용하지 않더라도 상황에 따라 캐스팅하는 것이 적합할 때 사용하는 연산자
  • 어떠한 정수 타입도 어떠한 포인터 타입으로 변환이 가능하고, 그 역도 가능

변환 관계에 놓인 두 개체의 관계가 명확하거나, 특정 목적을 달성할 때에만 사용하는 것이 바람직하다

dynamic_cast

런타임시에 형변환을 검사하여 형변환을 보다 안전하게 처리하는 연산자

  • 런타임에 동적으로 상속 계층 관계를 가로지르거나 다운 캐스팅시 사용
  • 포인터나 참조를 캐스팅할 때 사용 가능
  • 런타임에 객체의 타입 정보를 검사하는데 적합하지 않다고 판단하면 포인터에 대해서는 nullptr을 반환하고 참조에 대해서는 예외(참조자)를 반환합니다
  • 상속 관계에 있지만 virtual 멤버 함수가 하나도 없다면 다형성을 가진게 아니라 단형성이며, dynamic_cast는 다형성을 띄지 않는 객체간 변환을 불가능하며, 컴파일 에러가 발생한다
  • 변환 비용이 비싸다
class Base
{
public:
    virtual ~Base() = default;
};
 
class Derived : public Base
{
public:
    virtual ~Derived() = default;
};

int main()
{
	// 올바른 사용 예시
	Base* b;
    Derived* d = new Derived();
    b = d;
    d = dynamic_cast<Derived*>(b);
}

std::bit_cast

C++20에서 추가된 연산자
위의 연산자들은 C++ 언어의 일부이지만 이 연산자는 표준 라이브러리의 일부이다

  • reinterpret_cast와 비슷하지만 bit_cast는 주어진 타겟 타입의 새로운 객체를 생성하고 원본 객체를 비트로 복사한다
  • bit_cast는 효과적으로 소스 객체를 비트로 해석함
  • bit_cast를 사용할 때는 소스와 타겟 객체의 크기가 같아야 하고, 둘다 복사 가능한 형식이어야 한다
  • bit_cast는 단순 복사 가능 타입에 대해 바이너리 I/O를 수행하는 경우에 사용될 수 있다

    단순 복사 가능 타입이란 객체를 구성하는 내부 바이트를 char과 같은 타입의 배열처럼 비트 단위 복사로 쉽게 변환할 수 있는 타입

참고

marmelo12
별준님블로그
ence2.github.io

profile
나태지옥

0개의 댓글