CPP Module 05
“CPP Module 05에 대하여”
- 참고
- ex00: Mommy, when I grow up, I want to be a bureaucrat!
- ex01: Form up, maggots!
- ex02: No, you need form 28B, not 28C…
- ex03: At least this beats coffee-making
Repetition and Exceptions
난 if-else 보다는 try-catch가 편하던데…
참고
- try-throw-catch를 사용한 예외 처리에 대해 아주 자세히 공부하고 싶다면, 예외 처리 링크 참조.
ex00: Mommy, when I grow up, I want to be a bureaucrat!
사용자 정의 exception 클래스 만들기
참고 글을 읽어보았다면, try-catch의 대략적인 사용법에 대해 알고 있을 것이다. 우리가 주목해야 할 부분은 사용자 정의 클래스를 통해 throw() 할 수 있다는 사실이다. 클래스를 생성하거나(Bureaucrat, Form 등), 등급을 조정할 때 발생하는 각 예외 상황에 대해 우리가 직접 사용자 정의 클래스를 만들고 예외 처리를 하는 것이 이번 모듈의 전체적인 과업이 되겠다.
사용자 정의 클래스를 만들기 위해 hpp 파일에 특별히
<exception>
이라는 헤더를 포함시켜야 한다. exception 헤더는 C++ 표준 예외 클래스std::exception
을 정의한다. 이 클래스는 모든 표준 예외 클래스들의 기본 클래스이다. 즉, 우리가 만드는 모든 사용자 정의 예외 처리 클래스가 이 std::exception을 상속하게 된다는 말이다. 이 std::exception은 예외 처리 시 기본적인 메시지를 제공하기 위한 what() 멤버 함수를 가지고 있다. 이것은 예외가 발생했을 때 예외에 대한 설명을 반환한다. 코드를 만들며 살펴보도록 하자.
Bureaucrat::Bureaucrat(const std::string name, int grade) : name(name), grade(grade)
{
if (grade < 1)
throw Bureaucrat::GradeTooHighException();
else if (grade > 150)
throw Bureaucrat::GradeTooLowException();
}
위는 name과 grade를 받아 Bureaucrat 객체를 생성하는 생성자이다. 조건에 따라 예외를 던지는데, grade가 1미만이면
Bureaucrat::GradeTooHighException();
을, grade가 150을 초과한다면Bureaucrat::GradeTooLowException();
을 던진다.물론 GradeTooHighException과 GradeTooLowException은 Bureaucrat 클래스 내부에 정의된 사용자 정의 클래스이다. Bureaucrat.hpp에 위 클래스들을 정의해야 사용할 수 있을 것이다. 어떤 식으로 정의하면 될까?
// Bureaucrat.hpp
#pragma once
# include <iostream>
# include <string>
# include <exception> // exception 헤더가 필요하다고 했다.
class Bureaucrat
{
private:
const std::string name;
int grade;
public:
// ... OCCF나 Getter, Setter들
class GradeTooHighException : public std::exception
{
public:
const char *what() const throw();
};
class GradeTooLowException : public std::exception
{
public:
const char *what() const throw();
};
// throw() is Dynamic Exception Specification,
// we can expect that this function will not occur
// any type of exception explicitly.
}
사용자 정의 예외 처리 클래스를 정의하기 위해 exception 헤더를 정의했다. 우리가 정의하는 예외 처리 클래스들은 std::exception을 상속하며, 그 기능(
what()
)을 사용하여 예외 처리를 할 수 있다. public 아래에GradeTooHighException
와GradeTooLowException
클래스를 정의해놓았다. 한 가지 신경쓰이는 점이 있다. 클래스 내부에 다른 클래스를 정의해놓아도 될까?물론 문제될 것이 없다. 따로 써서 가독성을 향상시켜볼 수도 있겠지만, 제출 파일에 달리 exception에 대한 hpp 파일을 따로 지정해주지 않았으므로 class에서 사용할 예외 처리 클래스는 각자 class 내에 내부 클래스로 구현해보도록 하자.
what()
- std::exception에는 아래와 같이 정의되어 있다.
// exception
class _LIBCPP_EXCEPTION_ABI exception
{
public:
_LIBCPP_INLINE_VISIBILITY exception() _NOEXCEPT {}
virtual ~exception() _NOEXCEPT;
virtual const char* what() const _NOEXCEPT;
};
virtual const char* what()
은 exception에 정의된 가상 함수 메서드로, std::exception을 상속한 사용자 정의 클래스에서 오버라이드 될 수 있다.what()
은 발생한 Exception 객체의 종류를 문자열로 정의한다. 예를 들어GradeTooHighException
의 what()은 다음과 같이 정의될 수 있다.
const char* Bureaucrat::GradeTooHighException::what(void) const throw()
{
return ("Grade is too high...");
}
- Bureaucrat 프로그램이 진행되다가 GradeTooHighException 예외가 발생했을 경우, what()이 호출되며 사용자는 GradeTooHighException 예외가 발생했다는 것을 알 수 있다.

예외 사양(Exception Specification)
const char *what() const throw();
- 각 exception 클래스의 what이 위와 같이 정의되어 있는 것을 확인할 수 있을 것이다. 저 옆에
throw()
는 무슨 의미일까? 저 throw는 예외 사양(Exception Specification)이라고 하며, 함수가 던질 수 있는 예외의 종류를 명시한다. 예를 들어
void myfunc1(int n) throw(std::runtime_error, std::logic_error);
- 위 코드에서
myfunc1(int n)
에서는 std::runtime_error나 std::logic_error, 혹은 이들의 파생 클래스로 예외를 던질 수 있다.
void myfunc2(int n) throw();
함수 func_2 자체에서는 예외를 던지지 않는다는 것을 나타낸다. 이런 식으로 함수 원형이나, 정의부에 기재된 예외 사양을 통해 함수가 어떤 형태의 예외를 던지는 지 알 수 있다.
그러나 C++11 이후 버전에서 throw()를 사용한 예외 사양은 중요성을 잃었고, C++17에 이르러서는 더 이상 throw()로 예외 사양을 확인하지 않는다. 대신, noexcept를 사용하여 예외가 발생하지 않는다는 것을 명시한다. 현대 c++ 문법으로
GradeTooLowException
클래스를 정의하자면 아래와 같을 것이다.
class GradeTooLowException : public std::exception
{
public:
const char* what() const noexcept override;
};
- noexcept를 통해, what() 함수 자체로는 어떠한 예외도 발생시키지 않는다는 점을 알 수 있고, override를 통해 이 함수가
std::exception::what()
을 오버라이드했다는 점을 알 수 있다. c++98에서보다 직관성이 뛰어난 것 같다고 생각한다.요즘 세상에 누가 std++98로 코딩하나요.
ex01: Form up, maggots!
- Bureaucrat의
GradeToo...
예외 클래스들을 제대로 구현했다면, Form의 예외 클래스들 또한 무리 없이 구현할 수 있다. 예외 클래스들을 구현하고,beSigned()
나signForm()
과 같은 함수들도, Form과 Bureaucrat의 관계를 생각하며 구현해보자.
ex02: No, you need form 28B, not 28C…
만들어야 할 클래스와 함수들이 급격하게 늘어나서 좀 당혹스러울 수도 있다. 원래 만들어두었던 Form을 추상 클래스 AForm으로 바꾸고, 그 AForm을 상속하는 3가지 형태의 파생 Form
ShrubberyCreationForm
,RobotomyRequestForm
,PresidentialPardonForm
을 만들어야 한다. ShrubberyCreationForm이 조금 헷갈릴 수 있는데, 예전에 모듈 1에서 파일 열고 문자열 수정한 사본을 만들었던 기억을 되짚어보면 감이 좀 올지도 모른다. RobotomyRequestForm의 경우 랜덤 값을 받아야 하는데, c 함수를 사용해도 상관없다.rand()
혹은time()
등등을 사용할 수 있을 것 같다. 방법은 자유다. PresidentialPardonForm은 그냥 통보만 해주면 되는 어렵지 않은 함수다.Bureaucrat과 Form들의 관계를 생각하며,
execute(Bureaucrat const & executor)
나executeForm(Form const & form)
등의 함수들도 구현해보자.
const cast
c++에서 지원하는 강제 다형성(casting) 4인방
static_cast
,const_cast
,reinterpret_cast
,dynamic_cast
중의 하나인 const cast이다.ex00을 만들면서부터 이상함을 느꼈을 것이다. 할당 대입 연산자를 만들어야 하는데, 아무래도 이름을 변경할 수가 없도록 되어 있다. 그게 정상이다. const로 선언한 이름을 바꾸려고 하기 때문이다.
A Bureaucrat must have:
• A constant name. (const std::string name)
• And a grade that ranges from 1 (highest possible grade) to 150 (lowest possible grade).
- 이걸 어떻게 하면 좋을까하고 더러 질문을 받기도 했는데, 나 같은 경우에는 이 경우 할당 대입 연산자를 만들 때 그냥 grade만 복사해주라고 대답해준다(const 아닌 것만 복사). 물론 const cast를 사용하면 const로 선언된 this의 constant name(default)을 변경해줄 수 있긴 하지만, 애초에 const로 선언한 이름을 변경한다는 것 자체가 비상식적이다. grade만 복사해주면 어떤 일이 벌어질까?
Bureaucrat::Bureaucrat() : name("Default"), grade(150) {}
//Have to use const cast to assign properly
Bureaucrat& Bureaucrat::operator=(const Bureaucrat &ref)
{
if (this != &ref)
{
// *(const_cast<std::string*>(&this->name)) = ref.getName();
this->grade = ref.getGrade();
}
return (*this);
}
#include "Bureaucrat.hpp"
int main()
{
Bureaucrat bur("James", 2);
Bureaucrat dummy;
dummy = bur;
std::cout << bur.getName() << std::endl;
std::cout << dummy.getName() << std::endl;
return 0;
}
❯ ./Bureaucrat
James
Default
- 위는 내가 만들어놓은 할당 대입 연산자와 예시 main문이다. 우선 const cast를 사용한 부분은 주석 처리 하였다. dummy에 bur을 할당 대입했지만 둘의 getName은 다른 이름을 출력한다. dummy가 초기화되던 순간 default로 선언된 이름은, bur을 대입하더라도 변경되지 않고 여전히 default로 남아있게 된다. 사실 논리적으로 생각해도 이게 맞다. 각각의 Bureaucrat은 고유한 존재로, 복사 생성되거나 대입될 수 없다고 여겨지기 때문이다(
하지만 인간 시대의 끝이 도래한다면??). const cast는 이것을 깨트리고, dummy의 이름을 James로 바꿀 수 있도록 한다. 위의 const cast 주석을 풀고 결과를 출력한다면 어떻게 될까?
❯ ./Bureaucrat
James
James
- 졸지에 James가 두명이 되었다.
실현된 인간 복제의 꿈. const cast를 사용했기 때문에 가능한 일이다. 사용법을 알아보도록 하자.
*(const_cast<std::string*>(&this->name)) = ref.getName();
- const cast는 반드시 const 포인터에만 적용될 수 있다. 위 경우, 본래 const std::string 포인터인 this->name을 std::string 포인터로 cast한 후 역참조하여 ref.getName()을 대입해주었다. 따라서 이 라인에서 상수성이 제거되어 일시적으로 이름을 변경할 수 있게 된다. 포인터를 찍어보면서 살펴보자.
Bureaucrat& Bureaucrat::operator=(const Bureaucrat &ref)
{
if (this != &ref)
{
std::cout << "------------" << std::endl;
std::cout << "0: " << (*this).name << std::endl;
std::cout << "1: " << &(this->name) << std::endl;
*(const_cast<std::string*>(&this->name)) = ref.getName();
std::cout << "2: " << &(this->name) << std::endl;
std::cout << "3: " << this->name << std::endl;
std::cout << "4: " << (*this).name << std::endl;
std::cout << "------------" << std::endl;
this->grade = ref.getGrade();
this->name = "Can I change it?" << std::endl; // Compile Error!!
}
return (*this);
}
#include "Bureaucrat.hpp"
int main()
{
Bureaucrat bur("James", 2);
Bureaucrat dummy;
dummy = bur;
std::cout << bur.getName() << std::endl;
std::cout << dummy.getName() << std::endl;
return 0;
}
❯ ./Bureaucrat
------------
0: Default
1: 0x7ffee8763500
2: 0x7ffee8763500
3: James
4: James
------------
James
James
- 상수성은 일시적으로 제거된 것이기 때문에, const cast없이 다시 이름을 바꾸려고 하면 컴파일 에러가 발생한다. value와 포인터를 살펴본 결과 포인터 값은 그대로 유지되었고, value의 값만 변경된 것을 확인할 수 있었다. const 값을 변경할 수 있기 때문에 const cast는 신중히 사용해야 한다고 한다. const cast를 사용하지 않아도 모듈을 진행하는데 아무런 문제가 없지만, 한번쯤 맛보는 것도 괜찮다고 생각한다. Bureaucrat뿐만 아니라, Form 클래스에서도 const cast를 사용해볼 수 있다.
Upcasting & Downcasting
가상 함수와 다이나믹 캐스팅에 대해 자세히 공부하고 싶다면, 가상(virtual)함수와 다형성 참조. virtual에 대해 공부할 때 살펴본 적이 있는 링크일 것이다.
- 업 캐스팅과 다운 캐스팅에 대해서도 이야기해야 할 것 같다. 내가 모듈 5를 만들 당시에는 이 부분이 너무 어려웠어서, 다른 사람의 코드를 많이 참고한 것으로 기억한다. 어려웠던 기억이 있는 만큼, 이야기가 길어지게 될 것 같다. 업 캐스팅/다운 캐스팅의 경우 모듈 6에서도 이야기되고, 개인적으로는 런타임 다형성의 essence라고 생각하기 때문에 기본적인 개념 정도는 알아두는 것이 좋다고 생각한다.
> 기반 클래스 / 파생 클래스 간의 캐스팅은 기본적으로 포인터 를 통해 이루어진다.
- 바로 위에서 살펴보았던 const cast가 const 포인터를 대상으로 했던 것과 동일하다. 업 캐스팅/다운 캐스팅은 기반 클래스 포인터와 파생 클래스 포인터 간에 일어나는 캐스팅이다.
포인터와 동적 할당
- 스택에 선언된 지역 변수가 함수 스코프를 벗어나면 콜 스택에서 pop된다는 사실은 자명하다. 포인터를 사용해서 스코프 바깥에서 변수에 접근할 수 있다는 것 또한 마찬가지이다.
int* GetAddress()
{
int num = 10; //지역변수 선언
int* pNum = # //지역변수 선언 및 초기화
return pNum; //pNUm이 가지는 지역변수 num의 주소를 반환
}
int main(void)
{
int* pNum = GetAddress(); //반환된 주소로 초기화
std::cout << *pNum << std::endl;
//제어권을 잃은 상태로서, 반환된 주소에 해당하는
//메모리에 남은 값을 출력
return 0;
}
C로 코드를 작성하면서 malloc을 하지 않고 포인터를 사용했을 때 오류가 나는 경험이 자주 있었고, 관습적으로 포인터를 다룰 때 malloc을 사용하여 Heap에 메모리를 할당한 바 있다. Heap에 메모리를 할당하지 않고 포인터를 반환하게 되면 그 포인터가 가리키는 메모리는 임시 값이 되어 언제 값이 바뀔 지 모르는 상태가 된다. 본질적으로 함수 내에서 선언된 모든 변수는 지역 변수이며 함수 스코프를 벗어나게 되면 임시 값이 된다. 즉 예시 코드의 int* pNum도 지역 변수라는 것이다. 이는 굉장히 착각하기 쉬운 내용이다.
프로그래머가 malloc이나, new를 사용하여 Heap에 할당한 메모리는 프로그래머가 명시적으로 해제하거나, 프로그램이 종료될 때까지 남아있는다는 사실은 자명하다. 프로그래머가 메모리를 제대로 관리하지 않으면 메모리의 누수가 발생할 수 있다는 것 또한 마찬가지이다. 이는 주의해야 할 부분이다. 포인터에 대해 짚고 넘어간 것은 상속된 클래스 사이의 캐스팅이 포인터를 통해 일어나기 때문이다.
업 캐스팅
> 업 캐스팅(Upcasting)이란, 기반 클래스의 포인터로 파생 클래스 객체를 가리키는 것이다.
// UpCasting.cpp
#include <iostream>
#include <string>
using namespace std;
class Base
{
private:
public:
void what() { cout<< "Base" <<endl; }
};
class Derived : public Base
{
public:
void what() { cout<< "Derived" <<endl; }
};
int main(void)
{
Base *bp;
Base bas;
Derived der;
bp = &der; // ---> UP CASTING
bas.what(); // ---> print "Base"
bp->what(); // ---> also print "Base"
return (0);
}
- Base 포인터인 bp로 하여금, Derived 객체인 der를 가리키게 하였다. bp는 Base 포인터지만, 실질적으로는 Derived 객체인 der를 가리킨다.
bp->what()
를 출력하면 Derived::what()이 아니라 Base::what()이 출력된다. 이렇게 파생 클래스에서 기반 클래스로 캐스팅하는 것을 업 캐스팅이라고 한다.

Derived der 자체는 Base의
what()
과 Derived의what()
의 정보를 둘 다 가지고 있지만, bp는 Base 포인터이기 때문에 컴파일러는 bp는 Base의what()
을 실행시키게 된다.bp는 오로지 기반 클래스인 Base의 관한 정보에만 접근할 것이기 때문에 위와 같은 업 캐스팅에서는 아무런 문제도 발생하지 않는다. 하지만 반대의 경우엔 어떨까?
다운 캐스팅
> 상식적으로 생각했을 때, 다운 캐스팅(Downcasting)은 업 캐스팅의 반대이다. 하지만 파생 클래스는 기반 클래스 기능의 +α 이기 때문에 파생 클래스의 포인터가 기반 클래스 객체를 가리킬 수는 없다(없는 부분을 가리키게 된다).
class Base
{
private:
public:
virtual void what() { cout<<"Base"<<endl; }
// virtual 키워드 사용
};
class Derived : public Base
{
public:
void what() { cout<<"Derived"<<endl; }
};
int main(void)
{
Derived *dp1;
Base bas1;
Derived der1;
//dp1 = &bas; // This causes ERROR!
// dp1->what()을 호출하는 것이 불가능함
bas1.what(); // This prints "Base"
dp1 = nullptr; // OK
return 0;
}

만약 Derived *dp1이
what()
을 호출한다면, Base 객체인 bas1은Derived::what()
을 호출해야 하는데, Base 객체인 bas1은 자체적으로는 Derived 객체에 대한 정보를 가지고 있지 않기 때문에 문제가 생길 수 있다. 따라서 컴파일러 상에서는 함부로 다운 캐스팅 하는 것을 금지하고 있다.하지만 아래는 어떨까?
int main(void)
{
Base b1;
Derived d1;
Base *bp1 = &d1; // ok!
Derived *dp1 = bp1; // This cause Error too.
}
- bp1은 사실 Derived 객체를 가리키고 있기 때문에
Derived dp1 = bp1
처럼 사용하는 것이 완전히 불가능하다고 생각하지 않을 수도 있긴 한데, 일단은 이런 식으로 사용하는 것 또한 불가능하다. 우리가 알고 있는 static_cast를 사용하면, 가능하게 만들 수도 있다.
int main(void)
{
Base b1;
Derived d1;
Base *bp1 = &d1; // ok!
Derived *dp1 = static_cast<Derived *>(bp1); // ok!
dp1->what(); // print Derived
}
❯ ./a.out
Derived
- 이것은 bp1이 사실 Derived 객체를 가리키고 있다는 것을 알고 있기 때문에 가능한 일이다. 물론 bp1이 Derived 객체를 가리키고 있지 않는데, 강제로 static cast를 해서 Derived 포인터가 Base 객체를 가리키게 할 수 있다. 그러고 프로그램을 실행시키면 어떻게 될까?
int main(void)
{
Base b1;
Base *bp1 = &b1; // ok!
Derived *dp1 = static_cast<Derived *>(bp1); // ?????
dp1->what(); // 헉
}
❯ ./a.out
Base
- 클러스터 맥에서는 Base가 출력되었다. dp1은 Derived 포인터이기 때문에 Derived::what()을 출력하려고 할테고, Base 객체를 가리키는 bp1에는 Derived::what()에 그 정보가 없어서 런타임에 치명적인 문제가 발생할 것이라 생각했는데, 생각보다 얌전한 값을 출력해서 의외였다. 이 부분에 대한 케어가 되어서 일단 Base::what()을 호출했거나, 정의되지 않은 동작이라 임의의 값을 출력했다고 생각해볼 수 있을 것 같다. 물론 어느 쪽이든 정상적인 동작은 아니니, 굳이 시도해볼 필요는 없을 것 같다.
Dynamic Cast
- 이런 식으로 발생하는 오류를 방지하기 위해, c++에서는 상속 관계에 있는 두 포인터 간의 캐스팅을 지원하는 dynamic cast를 사용할 수 있다. static cast와 사용법이 거의 동일하다. 강제로 static cast한 코드를 다시 가져와서 static cast를 dynamic cast로 바꿔보자.
int main(void)
{
Base b1;
Base *bp1 = &b1; // ok!
Derived *dp1 = dynamic_cast<Derived *>(bp1); // static cast -> dynamic cast
dp1->what(); // 과연?
}
❯ ./a.out
zsh: segmentation fault ./a.out
- dp1이 가리키는 base 객체에 Derived::what()이 없어서 세그멘테이션 오류가 발생했다. bp1이 Derived 객체를 가리키도록 수정한다면, 정상적인 값이 출력될 것이다.
int main(void)
{
Derived d1;
Base *bp1 = &d1; // ok!
Derived *dp1 = dynamic_cast<Derived *>(bp1);
dp1->what();
}
❯ ./a.out
Derived
- 이제 dynamic cast를 사용해 다운 캐스팅 할 수 있다는 것을 알았다. dynamic cast를 사용해도 결국 Base 포인터가 실제로 Derived 객체를 가리키지 않는다면 오류가 발생하기 때문에, 포인터가 실제로 무슨 객체를 가리키는지 잘 알아보고 사용해야 한다. 추가적으로 dynamic cast를 사용하기 위한 조건을 살펴보고 과제로 넘어가보자.
** Dynamic cast를 사용하기 위한 조건 **
(1) 상속 관계 안에서만 사용할 수 있다. (당연하다)
(2) 하나 이상의 가상 함수를 가지고 있어야 한다. (기반 클래스에 virtual 키워드가 존재해야 한다)
(3) 컴파일러의 RTTI 설정이 켜져 있어야 한다. (이건 모듈 6에서 다뤄볼 것이다)
- virtual 함수가 없으면, 클래스가 polymorphistic 하지 않다는 컴파일 에러가 발생한다. polymorphistic 하지 않은 상속 관계의 클래스에서 static cast를 오용하면 아래와 같은 끔찍한 에러를 볼 수도 있다.

AForm과 파생 form 간의 dynamic cast
- 앞에서 실컷 업 캐스팅, 다운 캐스팅 이야기를 한 이유다. 파생 form의 할당 대입 연산자를 작성할 때, dynamic cast를 사용하는 부분이 있다. 아래를 보자.
ShrubberyCreationForm& ShrubberyCreationForm::operator=(const ShrubberyCreationForm& ref)
{
return *(dynamic_cast<ShrubberyCreationForm*>(&(AForm::operator=(ref))));
}
이 코드는 파생 클래스
ShrubberyCreationForm
의 대입 연산자 오버로딩 함수이다. 먼저 기반 클래스를 초기화하기 위해 기반 클래스 AForm의 대입 연산자 오버로딩 함수(AForm::operator=(ref)
)를 호출한다. 물론 그 매개변수는 ShrubberyCreationForm의 참조 ref(const ShrubberyCreationForm &ref
)이다. 파생 클래스 객체는 문제 없이 기반 클래스의 매개변수로 사용될 수 있다 (다형성의 기본 원리). 그러나AForm::operator=(ref)
의 반환값은 AForm의 참조(AForm&
)이기 때문에 업 캐스팅되어 AForm&으로 반환된다.그러나 ShrubberyCreationForm의 대입 연산자 오버로딩 함수의 반환값은
AForm&
이 아니라ShrubberyCreationForm&
이기 때문에 형변환이 필요하다. 위에서 말했듯이 파생 클래스 객체가 기반 클래스 객체로 바뀌는 것은 에러로 간주되기 때문에 이 경우 dynamic cast를 사용한 형변환이 필요하다. AForm& 앞에 붙여준 &은 dynamic cast가 기본적으로 포인터를 통해 이루어지기 때문에 붙여준 것이다. 즉 dynamic cast는AForm → ShrubberyCreationForm
으로 일어나는 것이 아니라AForm* → ShrubberyCreationForm*
으로 일어나게 되는 것이다. 물론 반환은 역참조해서 ShrubberyCrationForm 객체로 반환한다(ShrubberyCreationForm&
).한눈에 보자면 ShruberryCreationForm의 할당 대입 연산자는, 그 매개 변수
const ShruberryCreationForm& ref
가 AForm의 할당 대입 연산자의 매개 변수로 들어가 AForm&으로 반환되고(AForm::operator=(ref)
), 그 반환값 AForm&이 dynamic_cast되어(원래 shrubbery였으니까 가능) ShruberryCreationForm&으로 반환되는 3단 변신 코드다.
const ShruberryCreationForm& -> AForm& -> ShruberryCreationForm&
(AForm의 할당 대입 연산자) (dynamic_cast)
- 한 줄짜리 코드를 이해하기 위해 필요한 사전 지식의 양이 꽤 되는 편이다. 고수의 코드를 보고 따라쳤던 거긴 한데, 그때는 따라치는 것도 힘겨웠던 것 같다. 사실 이렇게 캐스팅하지 않고 일일히 대입할 수도 있었던 것 같긴 한데, 한줄에 깔끔하게 파생 Form을 대입할 수 있기 때문에 꽤 멋진 코드라고 생각해서 따라치면서 공부했던 것 같다.
슬라이싱(Slicing)
- 위에서는 할당 대입 연산자에 대해 살펴봤는데, 복사 생성자에 대해서도 살펴보자.
PresidentialPardonForm::PresidentialPardonForm(const PresidentialPardonForm &ref) \
: AForm(ref)
{
setType(PPF); // define PPF "PresidentialPardonForm"
}
마찬가지로 매개 변수로 들어온 상수 PresidentialPardonForm의 참조(
const PresidentialPardonForm &ref
)로 기반 클래스 AForm의 복사 생성자를 호출하는 것을 확인할 수 있다. 위에서도 살짝 이야기한 바 있는데, AForm에 직접 파생 클래스의 참조를 인자로 받는 복사 생성자가 정의되어 있지 않더라도, AForm은 파생 Form들의 참조를 인자로 받을 수가 있다. 그게 다형성의 기본적인 원리다.c++에서 파생 클래스 객체를 기반 클래스 복사 생성자를 사용하여 초기화할 때, 기본 클래스 부분은 기본 클래스의 복사 생성자를 사용하여 복사한다. 이때 객체의 슬라이싱(Slicing)이 발생한다. 즉 파생 클래스 객체가 기본 클래스 부분으로 줄어들게 된다. 위에서 업 캐스팅할때, Base 포인터가 Derived 객체의 모든 부분에 접근하지 않는 것과 비슷하다고 생각할 수도 있을 것 같다. 일종의 truncation이 발생한다고 여기면 되겠다. 이번 모듈의 경우, 중요한 정보인 멤버 변수들은 거의 AForm에 포함되어 있고, 파생 Form에는 그 Form을 대표하는 메인 함수들만 포함되어 있기 때문에 그 점이 좀 더 눈에 띈다.
ex03: At least this beats coffee-making
- 마지막으로 Intern 클래스가 등장한다. 별다른 특징(name, grade 등)없이 그저 폼만 동적 할당해내는 몰개성한 인턴 클래스를 만들면 된다. OCCF에
AForm* makeForm()
, 그리고 예외 처리 함수만 만들면 되기 때문에 그리 복잡하지는 않다.
You must avoid unreadable and ugly solutions like using a if/elseif/else forest.
- 서브젝트에는 다음과 같은 내용이 포함되어 있다. 어디선가 본 적이 있는 주문 사항같은데…? Harl 클래스가 머리를 스치고 지나간다. 그때 만들어둔 switch 문을 활용하면 왠지 편리하게 분기를 작성할 수 있을 것 같다. Form의 이름을 매개 변수로 받아 적절한 Form을 동적 할당하는 분기문을 작성해보자.
try-catch의 사용처에 대한 고찰 (중요)
- 이번 모듈을 진행하면서 try-catch를 하게 되는데, 내 생각에는 try-catch를 어디서 하느냐에 따라 작업량에 차이가 생기는 것 같다. 작업량이 적은 것은 main에서 try하고 main에서 catch하는 것이다.
#include "Bureaucrat.hpp"
int main()
{
try
{
try
{
Bureaucrat bur1("John", 0);
}
catch(const std::exception& e)
{
std::cerr<<BOLDRED<<e.what()<<std::endl;
}
Bureaucrat freshman("Fred", 149);
freshman.decrementGrade();
std::cout<<freshman<<std::endl;
}
catch(const std::exception& e)
{
std::cerr<<BOLDRED<<e.what()<<std::endl;
}
return (0);
}
- 예를 들어, 이것은 내가 ex00에서 작성했던 main 문이다. main에서 try하고, main에서 catch한다. 이렇게 하면 Bureaucrat의 어디서 throw 하든, 스택을 풀면서 main에서 catch 해줄 수가 있다.
Bureaucrat::Bureaucrat(const std::string name, int grade) : name(name), grade(grade)
{
if (grade < 1)
throw Bureaucrat::GradeTooHighException();
else if (grade > 150)
throw Bureaucrat::GradeTooLowException();
}
void Bureaucrat::incrementGrade()
{
this->grade -= 1;
if (this->grade < 1)
throw Bureaucrat::GradeTooHighException();
}
void Bureaucrat::decrementGrade()
{
this->grade += 1;
if (this->grade > 150)
throw Bureaucrat::GradeTooLowException();
}
예외가 생기는 경우, 그냥 던지기만 해버리면 main에서 받아줄 수가 있다. Form도 마찬가지다. ex01이나 ex02도 main에서 try-catch하면, Bureaucrat이나 Form, AForm의 cpp 파일에서는 마구 던져버리기만 하면 된다. 따라서 작업량이 줄게 된다.
그런데 ex03의 경우 main에서 try-catch 할 수가 없다. Form을 동적 할당하기 때문이다. 한번 살펴보자.
#include "Bureaucrat.hpp"
#include "AForm.hpp"
#include "ShrubberyCreationForm.hpp"
#include "RobotomyRequestForm.hpp"
#include "PresidentialPardonForm.hpp"
#include "Intern.hpp"
void leakcheck()
{
std::cout<<RESET<<std::endl;
system("leaks Intern | greps ex03");
}
int main()
{
atexit(leakcheck);
try {
// ...
Intern someRandomIntern;
AForm* rrf1;
AForm* rrf2;
AForm* rrf3;
rrf1 = someRandomIntern.makeForm("Shrubbery Creation", "Gwan");
rrf2 = someRandomIntern.makeForm("Robotomy Request", "Bender");
rrf3 = someRandomIntern.makeForm("Presidential Pardon", "Winter Soldier");
// ... 여기에서 throw하게 되면, delete하지 않고 곧바로 catch로 넘어가게 됨
delete rrf1;
delete rrf2;
delete rrf3;
}
catch (std::exception &e)
{
std::cout<<BOLDRED<<e.what()<<RESET<<std::endl;
}
}
- 인턴을 하나 만들고, 인턴이 세 개의 Form을 동적 할당했다. try 블록의 끄트머리에서 그 Form 들을 해제한다. 겉보기에는 별 문제 없어보이는 코드지만, rrf들을 동적 할당하는 부분과 rrf들을 해제하는 부분 사이에서 throw하고 그걸 main문의 catch에서 받으려 하면 해제하는 라인을 패스하고 곧바로 catch 블록으로 들어가기 때문에 필히 메모리 누수가 발생하게 된다. 예를 들어 …
Bureaucrat::Bureaucrat(const std::string name, int grade) : name(name), grade(grade)
{
if (grade < 1)
throw Bureaucrat::GradeTooHighException();
else if (grade > 150)
throw Bureaucrat::GradeTooLowException();
}
int main()
{
try
{
// ...
Intern someRandomIntern;
AForm* rrf1;
AForm* rrf2;
AForm* rrf3;
rrf1 = someRandomIntern.makeForm("Shrubbery Creation", "Gwan");
rrf2 = someRandomIntern.makeForm("Robotomy Request", "Bender");
rrf3 = someRandomIntern.makeForm("Presidential Pardon", "Winter Soldier");
Bureaucrat haha("Dummy", -1); // ???
delete rrf1;
delete rrf2;
delete rrf3;
}
catch (std::exception &e)
{
std::cout<<BOLDRED<<e.what()<<RESET<<std::endl;
}
}
❯ ./Intern
Intern creates Form, the type is <Shrubbery Creation>
Intern creates Form, the type is <Robotomy Request>
Intern creates Form, the type is <Presidential Pardon>
Grade is too Low...
Process 27499: 3 leaks for 240 total leaked bytes.
3 (240 bytes) << TOTAL >>
1 (80 bytes) ROOT LEAK: <PresidentialPardonForm 0x7f81c7405a00> [80]
1 (80 bytes) ROOT LEAK: <RobotomyRequestForm 0x7f81c74059b0> [80]
1 (80 bytes) ROOT LEAK: <ShrubberyCreationForm 0x7f81c7405960> [80]
- ex03의 main문의 경우 폼을 sign하는 Bureaucrat, execute하는 Bureaucrat, 폼을 만드는 Intern 등이 등장해야 할 것이므로 throw 할 수 있는 경우가 아주 많겠지만 여기서는 간단하게 너무 높은 등급을 가진 Bureaucrat 하나를 난입시켜보자. Bureaucrat의 생성자에서 throw가 발생하고 바로 catch 블록으로 넘어가게 되면서 폼 세개가 고스란히 누수를 일으키게 된다. 어떻게 폼들을 해제시킬 수 있을까? 이런 생각을 해볼 수 있을지도 모르겠다.
catch (std::exception &e)
{
delete rrf1;
delete rrf2;
delete rrf3;
std::cout<<BOLDRED<<e.what()<<RESET<<std::endl;
}
- 이렇게 catch 블록에서 해제해주면 안되나요? 아쉽게도 이런 식으로 쓰는 건 불가능하다. 다음과 같은 컴파일 에러가 발생하게 된다.
// ...
main.cpp:36:16: error: use of undeclared identifier 'rrf1'
delete rrf1;
^
main.cpp:37:16: error: use of undeclared identifier 'rrf2'
delete rrf2;
^
main.cpp:38:16: error: use of undeclared identifier 'rrf3'
delete rrf3;
^
3 errors generated.
make: *** [main.o] Error 1
- 이 문제를 해결하기 위해 여러 가지 방법을 생각해봤는데, 결국 가비지 콜렉터나 스마트 포인터를 쓰는 것밖에는 떠올리지 못했다. 근데 이거 해결하자고 잘 알지도 못하는 가비지 콜렉터, 스마트 포인터를 쓰자니 배보다 배꼽이 더 큰 형편이다. 결국 try-catch를 메인에서 하지 않는 것 밖에는 방법이 없다.
날먹 금지main에서 try-catch하지 않고, 예외가 발생할 수 있는 모든 부분에서 try-catch를 작성하는 것이다. 위의 Bureaucrat 생성자의 경우 …
Bureaucrat::Bureaucrat(const std::string name, int grade) : name(name), grade(grade)
{
if (grade < 1)
throw Bureaucrat::GradeTooHighException();
else if (grade > 150)
throw Bureaucrat::GradeTooLowException();
}
- 이렇게 throw만 하는게 아니라 …
Bureaucrat::Bureaucrat(std::string name, int grade) : name(name), grade(grade)
{
try // 예외가 발생하는 블록에서 곧바로 try-catch문 작성
{
if (grade < 1)
throw Bureaucrat::GradeTooHighException();
else if (grade > 150)
throw Bureaucrat::GradeTooLowException();
this->grade = grade;
}
catch (std::exception &e)
{
std::cout<<BOLDRED<<e.what()<<RESET<<std::endl;
}
}
int main()
{
// ... 메인에서 try-catch 하지 않음
Intern someRandomIntern;
AForm* rrf1;
AForm* rrf2;
AForm* rrf3;
rrf1 = someRandomIntern.makeForm("Shrubbery Creation", "Gwan");
rrf2 = someRandomIntern.makeForm("Robotomy Request", "Bender");
rrf3 = someRandomIntern.makeForm("Presidential Pardon", "Winter Soldier");
Bureaucrat haha("Dummy", -1);
// 여기서 throw 해도 코드가 끝나지 않고, 아래로 진행하면서 메모리를 해제함
delete rrf1; // 정상 진행
delete rrf2; // 정상 진행
delete rrf3; // 정상 진행
return 0;
}
- 요렇게 작성해주면 된다. 이러면 Bureaucrat을 생성할 때 예외가 발생해도, 같은 스택에서 바로 catch하면서 메모리 해제 라인을 패싱하지 않게 되고 메모리 누수가 생기지 않게 된다. 모든 예외 사항에 대해 그 자리에서 바로 try-catch 문을 작성해줘야 하니, 만약 main에서 try-catch 하는 코드를 계속해서 짜왔다면 main과 .cpp 파일들에 대한 대규모 수정을 감행해야만 한다. 물론 ex00부터 예외가 발생할 때마다 그 스택 내에서 바로바로 catch 해줬다면 해당 사항 없는 내용이기는 하다. 예외에 알맞는 exception을 작성하면서 과제를 마무리해보자.