해당 과제를 진행하는데 있어 필요한 사전 지식들을 정리하였습니다.
Module 00
에서 한 번 다루었듯이 C++는 다음의 4가지 타입 캐스트 연산자를 지원한다.
1) static_cast
포인터 → 포인터
/ 참조자 → 참조자
형변환 허용 (업 캐스팅 & 다운 캐스팅)2) dynamic_cast
포인터 → 포인터
/ 참조자 → 참조자
형변환 허용 (업 캐스팅)포인터 → 포인터
/ 참조자 → 참조자
형변환 허용 (다운 캐스팅)3) reinterpret_cast
정수 계열 → 포인터
/ 포인터 → 정수 계열
형변환 허용4) const_cast
ex00
의 목표는 문자열로 들어오는 입력을 받아 기본 자료형(char
, int
, float
, double
)으로 형변환을 한 뒤 출력하는 것이다. 기본 자료형 간의 형변환만 필요하기 때문에 static_cast
연산을 사용하면 된다. 입력으로 들어온 문자열이 해당 타입으로 변환할 수 없는 경우나 출력할 수 없는 경우, 각 상황에 맞게 Non displayable
, impossible
, nan
, nanf
, inf
, inff
, *
등을 출력해야 한다.
입력이 문자열이기 때문에 std::stoi
, std::stof
, std::stod
등을 통한 형변환을 하면 될 것 같지만 해당 함수들은 C++11
의 문법이기 때문에 std::atof
혹은 std::strtod
를 활용해 변환해야한다.
입력으로 들어오는 문자열 자체에 대한 유효성 검사를 진행 후 static_cast
연산을 통해 형변환을 하면 되는데 그 과정에서 numeric_limits
클래스 템플릿의 infinity()
, max()
, min()
, lowest()
등의 함수로 각 타입마다의 한 번의 더 유효성 검사를 거친 후 변환된 값을 출력하면 된다. 예외 처리는 try~catch
문으로 구성한다.
ex01
의 목표는 다음의 두 함수를 구현하는 것이다.
uintptr_t serialize(Data* ptr)
Data* deserialize(uintptr_t raw)
즉, 클래스 혹은 구조체인 Data의 포인터를 uintptr_t
로 변환하고, uintptr_t
를 다시 Data의 포인터로 변환하는 것이다. 이 과정에서 누락되는 데이터가 없는지 확인해야 하고, 데이터들은 온전히 유지되어야 한다.
intptr_t
와 uintptr_t
타입은 포인터의 주소를 저장하는데 사용된다. 이 두 타입은 다른 환경으로 이식이 가능하고 안전한 포인터 선언 방법을 제공하며, 시스템 내부에서 사용하는 포인터와 같은 크기다. 포인터를 정수 표현으로 변환할때 유용하게 사용할수 있다.
형변환은 reinterpret_cast
를 통해 가능하다.
uintptr_t serialize(Data *ptr)
{
return reinterpret_cast<uintptr_t>(ptr);
}
Data *deserialize(uintptr_t raw)
{
return reinterpret_cast<Data *>(raw);
}
가상 소멸자만 가지고 있는 Base 클래스와 해당 클래스를 상속하는 A, B, C 클래스를 정의한다. Base* generate(void)
함수를 통해 무작위로 A, B, C 중 하나의 객체를 생성하고 객체의 Base 포인터를 반환한다. (업 캐스팅) 그리고 void identify(Base* p)
와 void identify(Base& p)
함수를 통해 타입 p
가 A, B, C 중 어느 클래스에 해당하는지 찾는다.
한마디로 해당 과제의 목표는 상속 구조를 만들어 업 캐스팅 후, 다운 캐스팅을 통해 자신의 타입이 무엇인지 밝히는 것이다.
static_cast vs dynamic_cast
상속 구조를 갖는 객체들 간에는 기초 클래스를 메모리 상에 갖고 있기 때문에 유도 클래스를 기초 클래스의 포인터로 참조하는 것이 가능했고, 따라서 업 캐스팅 시에는 아무런 문제가 없었다. 하지만 유도 클래스가 업 캐스팅 된 기초 클래스 형태가 아니라 순수한 기초 클래스를 이용하는 경우라면, 유도 클래스를 메모리 상에 유지하고 있지 않기 때문에 유도 클래스로의 다운 캐스팅은 문제가 될 수 있다. 이 때문에 기본적으로 컴파일러는 다운 캐스팅을 금지하고, 이와 같은 시도를 하면 에러를 내준다.
유도 클래스 → 기초 클래스 → 유도 클래스
형태의 캐스팅은 문제가 되지 않는다. 따라서 해당 경우에는 기초 클래스를 유도 클래스로 변환하는 것이 가능하기 때문에 static_cast
를 이용해도 된다. 문제는 static_cast를 이용하면 유도 클래스 → 기초 클래스 → 유도 클래스
뿐만 아니라 기초 클래스 → 유도 클래스
에 대해서도 에러를 만들지 않기 때문에, 기초 클래스 → 유도 클래스
는 실행이 되어서야 런 타임 에러가 난다. 사전에 컴파일 타임에서 에러를 찾아낼 수 없는 것이다.
따라서 유도 클래스 → 기초 클래스 → 유도 클래스
는 허용하면서도, 기초 클래스 → 유도 클래스
에 대해서는 막을 수 있는 dynamic_cast
를 사용하는 것이 바람직하다. dynamic_cast
를 이용하면 기반 클래스 → 파생 클래스
에 대해서 컴파일 타임에 에러를 낸다.
다만 dynamic_cast
는 하나 이상의 가상함수를 가진 기초 클래스(다형성)에 대해서만 다운 캐스팅을 허용한다.
과제에서 요구하는 identify()
함수의 인자로는 포인터가 올 수도 있고 참조자가 올 수도 있다. dynamic_cast
의 형 변환 실패는 기본적으로 NULL을 기반으로 한다. 포인터의 경우 Base* p
에 대한 NULL은 존재할 수 있으므로 형 변환 실패 시 NULL이 반환되고, 참조자의 경우 NULL에 대한 참조는 불가능하기 때문에 exception을 던지게 된다.
따라서 void identify(Base* p)
의 경우는 NULL을 반환하지 않은 경우가 자기 자신의 타입이고 void identify(Base& p)
의 경우는 exception이 던져지지 않은 경우가 자기 자신의 타입이다. 후자의 경우는 자신의 타입이 아니면 exception을 던지기 때문에 try~catch
문으로 묶어주어야 한다.
참고
https://bigpel66.oopy.io/library/42/inner-circle/17
https://velog.io/@chaewonkang/CPP-STL-Cast
https://velog.io/@hey-chocopie/C-Module-06