서론
말그대로 C++의 방식대로 형변환을 해보는 과제이다
추가 룰로 각 ex마다 풀기 위해 특정한 하나의 형 변환을 사용해야 하는데 그 선택이 defense에 담겨있어야 한다.
ex00
string을 일반적인 자료형으로 명시적으로 변환하여 출력하는 프로그램을 만들어라. 일반적인 자료형이란 char, int, float, double을 의미한다.
char 예시 : ‘c’, ‘a’ … 출력할 수 없는 문자열이라면 출력하려 하지 말고 해당 정보를 알려주면 된다.
int 예시 : 0 -42 42 …
float 예시 : 0.0f, -4.2f, 4.2f … 추가로 -inff, +inff, nanf 도 받을 수 있어야 한다.
double 예시 : 0.0, -4.2, 4.2 … 추가로 -inf, +inf, nan 도 받을 수 있어야 한다.
문자를 탐지한 이후에 가장 올바른 타입으로 변환하고, 그다음에 세 개의 다른 타입으로 변환해서 출력해줘야 한다. 변환에 문제가 있거나 오버플로우 등이 발생하면 관련 메시지를 표시한다. 숫자 제한이나 특수 값과 관련된 헤더는 전부 사용할 수 있다.
static_cast를 사용해보는 과제이다. 기본적인 형 변환을 담당하고 있으며 컴파일 타임에 에러를 띄워주는 형 변환을 담당하고 있다.
이걸 쉽게 하려면 stoi 등의 string 함수들을 사용하면 되겠지만, 나는 아무리 생각해봐도 Any function to convert from a string to an int라는 말이 C++11 함수를 사용할 수 있는 말이라고는 이해되지가 않았다.
그래도 쉽게 하려면 그냥 무조건 double로 바꾼 다음 다시 처리하도록 만들면 되겠지만, 나는 convert it from string to its actual type, then convert it explicitly to the three other data types라는 말을 준수하고 싶었다.
그래서 모든 예외상황을 생각하면서 처리했는데 생각보다 너무 어려운 과제여서 ex00 하나에만 며칠을 써버렸다. 전자로 처리한 평가자를 만나면 반드시 컴파일 에러를 누르겠지만, 후자로 처리한 사람은 평가표에 없다면 문제 삼지 말아야겠다.
https://www.ryugod.com/pages/ide/cc98
맥에서 제대로 동작하지 않는 std=c++98을 확인해 볼 수 있는 ide
#!/bin/bash
echo "input: <0> "
./convert 0
echo "input: <10.0>"
./convert 10.0
echo "input: <1000>"
./convert 1000
echo "input: <f>"
./convert f
echo "input: <42.0f>"
./convert 42.0f
echo "input: <42.0fq>"
./convert 42.0fq
echo "input: <nan>"
./convert nan
echo "input: <inf>"
./convert inf
echo "input: <-inff>"
./convert -inff
echo "input: <test>"
./convert test
echo "input: <10.ewrewrf>"
./convert 10.ewrewrf
echo "input: <11111111>"
./convert 11111111
echo "input: <111111>"
./convert 111111
echo "input: <1111111111111>"
./convert 1111111111111
echo "input: <111111111111111111111111111111111111111111111111111111111>"
./convert 111111111111111111111111111111111111111111111111111111111
간단하게 예외 처리해야 하는 상황들을 미리 적어두고 하나씩 맞춰가면서 코딩을 했다. 뭐 하나 고칠 때마다 다른 부분이 고장 나는 미니쉘 같은 기분을 겪다가 만들었는데 유용하게 써먹었다.
void Convert::setValue(std::string s)
{
std::string::size_type n;
std::string::size_type f;
try
{
this->value = s;
}
catch(const std::bad_alloc& e)
{
err = true;
return ;
}
n = s.find('.');
f = s.find('f', s.length() - 1);
if (value == "nan" || value == "inf" || value == "+inf" || value == "-inf" || value == "nanf" || value == "inff" || value == "+inff" || value == "-inff")
{
if (value == "nan" || value == "nanf")
{
val_double = sqrt(-1.0);
}
else
{
if (value[0] == '-')
{
val_double = __DBL_MAX__ * -1000;
}
else
{
val_double = __DBL_MAX__ * 1000;
}
}
}
else if (s.length() == 1 && !std::isdigit(static_cast<char>(s[0])))
{
val_char = static_cast<char>(s[0]);
val_int = static_cast<int>(val_char);
val_float = static_cast<float>(val_char);
val_double = static_cast<double>(val_char);
}
else if (n == std::string::npos)
{
val_int = atoi(s.c_str());
std::stringstream ss;
ss << val_int;
std::cout << "len : " << s.length() << " size : " << s.size() << std::endl;
if (ss.str() != value)
{
if (val_int > 0 && f == s.length() - 1)
{
val_char = static_cast<char>(val_int);
val_float = static_cast<float>(val_int);
val_double = static_cast<double>(val_int);
}
else
{
err = true;
}
}
else
{
val_char = static_cast<char>(val_int);
val_float = static_cast<float>(val_int);
val_double = static_cast<double>(val_int);
}
}
else
{
const char *str = s.c_str();
char *end = NULL;
val_double = strtod(str, &end);
if((*end && !(*end == 'f' && end == &str[s.length() - 1])))
{
err = true;
}
else
{
val_int = static_cast<int>(val_double);
val_char = static_cast<char>(val_double);
val_float = static_cast<float>(val_double);
}
}
}
가장 먼저 가져온 문자열을 객체에 저장해준다. 만약 문자열이 지나치게 큰 경우 bad_alloc으로 인해 프로그램이 멈출 수 있기 때문에 멈추지 않게 예외 처리해준다.
nan이나 inf에 대한 처리를 먼저 해당 문자가 들어왔는지를 확인한 후 double 변수에 해당 값을 저장해준다.
문자열이 한 글자이면서 숫자가 아닌 경우 char로 저장해주고, 해당 값을 다른 변수에 static_cast 해서 처리한다.
size_type와 find를 이용하여 < . >과 < f >의 위치를 찾는다. f의 경우 마지막에 있는지만 확인한다.
만약 . 의 위치가 npos일 경우 해당 문자에는 . 이 없다는 뜻이므로 int라고 생각하고 처리하고 나머지는 실수로 처리한다.
다만 저장해둔 string과 변환한 값이 다를 경우 숫자가 아닌 문자열이거나, 오버플로우이거나, 끝에 f 가 붙은 float 형이므로 예외처리를 해준다.
std::ostream& operator<<(std::ostream &out, const Convert &b)
{
std::stringstream ss;
ss << b.getInt();
try
{
out << "char: " << b.printChar() << std::endl;
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
try
{
out << "int: " << b.printInt() << std::endl;
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
try
{
if (static_cast<float>(b.getInt()) == b.getFloat())
{
if (ss.str().size() > 6)
{
out << "float: " << b.printFloat() << std::endl;
}
else
{
out << "float: " << b.printFloat() << ".0f" << std::endl;
}
}
else
{
out << "float: " << b.printFloat() << "f" <<std::endl;
}
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
try
{
if (static_cast<double>(b.getInt()) == b.getDouble())
{
if (ss.str().size() > 6)
{
out << "double: " << b.printDouble() << std::endl;
}
else
{
out << "double: " << b.printDouble() << ".0" << std::endl;
}
}
else
{
out << "double: " << b.printDouble() << std::endl;
}
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
return (out);
}
출력 부분에서는 print 함수에서 에러를 throw 하여 에러 상황이나 nan inf 등의 상황을 처리하였다.
실수 출력에서 7자리 이상의 정수가 1.11111e+07 같은 형태로 출력되어서 끝에 일괄적으로. 0f를 붙이는 것으로 처리할 수가 없었다. 그래서 해당 실수가 정수와 동일하면서 문자열의 크기가 6보다 큰 경우를 예외 처리하였는데, 문제는 이게 6보다 큰 게 100% 라는 확신이 없었다.
IEEE 754의 가수부가 23비트이고 해당 값은 7자리이므로(8388608), 정확한 값을 표기하기 위해 중간에 예외 처리하느니 6자리까지만 표기한다….라고 추측은 했는데 그건 내 추측일 뿐이고, 정확한 이유는 찾지 못했지만 정밀도가 6 인건 문서화되어있는 것 같았다. 그래서 length가 6보다 클 때 따로 처리해줬는데 맞는 방법인지는 모르겠다. 뭔가 매크로로 정의가 되어있을 것 같았는데 그렇지는 않았다.
https://hwan-shell.tistory.com/211
https://www.delftstack.com/ko/howto/cpp/cpp-last-character-of-string/
https://ju3un.github.io/c++-new-excepiton/
https://dojang.io/mod/page/view.php?id=739
https://blockdmask.tistory.com/39
https://mg729.github.io/c++/2019/11/03/C++_string_npos/
https://en.cppreference.com/w/cpp/io/basic_ios/init
http://websites.umich.edu/~eecs381/handouts/formatting.pdf
ex01
다음 기능 두 개를 구현하라
Data의 포인터를 unsignd 정수 타입 uintptr_t로 변환하는 uintptr_t serialize(Data* ptr);
unsigned 정수를 Data의 포인터로 변환하는 Data* deserialize(uintptr_t raw);
값이 비어있지 않은 Data 구조체를 만들어야 한다.
Data 객체의 주소에 serialize를 사용하고 그 반환 값을 deserialize를 사용하여 해당 반환 값이 처음의 값과 동일한지를 확인하라.
struct Data
{
std::string name;
};
uintptr_t serialize(Data* ptr)
{
return(reinterpret_cast<uintptr_t>(ptr));
}
Data* deserialize(uintptr_t raw)
{
return(reinterpret_cast<Data *>(raw));
}
int main(int ac, char *av[])
{
Data prev;
Data *next;
uintptr_t ptr;
if (ac != 2)
{
std::cout << "argument count is not 2" << std::endl;
return (1);
}
prev.name = av[1];
std::cout << "prev : " << prev.name << std::endl;
ptr = serialize(&prev);
std::cout << "ptr : " << ptr << std::endl;
next = deserialize(ptr);
std::cout << "next : " << next->name << std::endl;
return (0);
}
reinterpret_cast를 사용해보는 과제이다.
reinterpret_cast란 주로 포인터에서 사용되는 캐스팅으로 포인터→포인터, 변수→포인터, 포인터→변수를 담당하고 있다.
이것의 용도는 과제의 이름과 함수명처럼 serialization, 즉 직렬화에서 주로 사용되는데 네트워크에서 정보를 보낼 때 stream으로 보내야 하는데, 당연히 이건 기본 자료형으로만 보낼 수 있다.
따라서 사용 중인 구조체나 클래스의 객체나 포인터의 정보를 보내기 위해서는 해당 정보를 기본 자료형으로 변환해줘야 하는데, 이를 직렬화라고 한다.
물론 위와 같은 작업으로는 네트워크 전송에는 택도 없지만, 객체를 네트워크에 보낼 수 있는 값으로 변환하고 그걸 받아서 다시 데이터로 만드는 작업을 해본다고 생각하면 될 것 같다.
https://hwan-shell.tistory.com/219
https://isocpp.org/wiki/faq/serialization
ex02
이 과제는 Orthodox Canonical Form을 사용하지 않아도 된다.
virtual 소멸자 하나만을 지닌 Base 클래스를 만들어라. 그다음 세 개의 빈 클래스 A, B, C를 만든다.
다음과 같은 기능을 구현하라
A, B, C 중 랜덤 한 클래스의 객체를 반환하는 Base * generate(void);
객체의 클래스가 무엇인지를 출력하는 void identify(Base* p); void identify(Base& p);
단, void identify(Base& p); 를 구현할 때는 포인터 사용 금지.
typeinfo 헤더는 금지되며 테스터는 직접 만들어야 한다.
void identify(Base* p)
{
if (dynamic_cast<A*>(p))
std::cout << "pointer is A\n";
if (dynamic_cast<B*>(p))
std::cout << "pointer is B\n";
if (dynamic_cast<C*>(p))
std::cout << "pointer is C\n";
}
void identify(Base& p)
{
try
{
A &a = dynamic_cast<A&>(p);
std::cout << "reference is A\n";
static_cast<void>(a);
}
catch (std::exception&) {}
try
{
B &b = dynamic_cast<B&>(p);
std::cout << "reference is B\n";
static_cast<void>(b);
}
catch (std::exception&) {}
try {
C &c = dynamic_cast<C&>(p);
std::cout << "reference is C\n";
static_cast<void>(c);
}
catch (std::exception&) {}
}
dynamic_cast를 사용해보는 과제이다.
dynamic_cast는 주로 부모를 가리키는 포인터를 자식을 가리키도록 바꾸는 다운 캐스팅에 사용되는 캐스팅이다. 만약 부모 클래스가 가상 함수가 있는 추상 클래스라면 자식의 주소를 따로 저장해두기 때문에(클랩 트랩에서 지나가듯 설명함) 다운 캐스팅이 가능하다.
다만, 자식의 생성자가 호출되지 않은 상태라면(부모 클래스의 포인터를 부모 클래스의 생성자에 부른 경우) 당연히 참조할 주소가 없으니 불가능하며, 추상 클래스가 아니라면 dynamic_cast가 아닌 static_cast를 사용해야 한다.(단, 이 경우 안전은 보장되지 않는다.)
dynamic_cast를 자식 클래스에서 자식 클래스로 형 변환할 경우 당연히 다른 자식 클래스의 생성자를 받지 못했을 테니 실패하게 된다.
인자를 포인터로 받느냐 레퍼런스로 받느냐에 따라 구현 방식이 바뀌는데, dynamic_cast를 실패했을 때 포인터일 경우 NULL을 반환하지만 레퍼런스의 경우(NULL을 못쓰니) exception이 일어난다.
따라서 포인터로 받을 때는 if else문을 사용하여 처리해주고, 레퍼런스로 받을 때는 try catch문을 사용하여 처리해주면 된다.
https://hwan-shell.tistory.com/213
https://blockdmask.tistory.com/241
https://musket-ade.tistory.com/entry/C-C-형-변환-연산자-staticcast-constcast-dynamiccast-reinterpretcast
https://docs.microsoft.com/ko-kr/cpp/cpp/dynamic-cast-operator?view=msvc-170
https://stackoverflow.com/questions/7343833/srand-why-call-it-only-once
'프로그래밍' 카테고리의 다른 글
[42서울] CPP Module 08 - STL (1) | 2022.09.11 |
---|---|
[42서울] CPP Module 07 - 템플릿 (1) | 2022.09.11 |
[42서울] CPP Module 05 - 재사용성과 예외처리 (3) | 2022.09.10 |
[42서울] CPP Module 04 - 다형성과 추상클래스 (0) | 2022.09.10 |
[42서울] CPP Module 03 - 클래스 상속 (0) | 2022.08.18 |