본문 바로가기

프로그래밍

[42서울] CPP Module 05 - 재사용성과 예외처리

서론

힌남노 때문인지 인트라가 맛이 가기 직전인 날이었습니다.

 

재사용성과 예외처리에 대한 과제이다.

 

try-catch문과 exception을 사용해보면서 예외처리를 몸에 익히는 과제라고 생각하면 될 것 같다.

 

 

ex00

거대한 관료체계의 관료 클래스를 만들어라

 

상수 name과 1~150 사이의 grade을 보유해야 한다.

 

만약 잘못된 등급을 받게 되면 Bureaucrat::GradeTooHighException이나 Bureaucrat::GradeTooLowException 함수를 호출하여 예외처리를 해야 한다.

 

생성자에서도 에러 발생 시 동일한 함수로 예외처리를 하라.

 

getName과 getGrade 함수를 만들고 grade를 높이거나 낮추는 함수도 보유해야 한다. 1등급이 가장 높기 때문에 3의 grade를 높인다면 2가 된다.

 

<< 를 오버 로딩하여 <name>, bureaucrat grade <grade>. 를 출력할 수 있게 하고 필요한 테스트를 구현하라

 

 

class Bureaucrat {
 private:
	const std::string name;
	int grade;
 public:
	class GradeTooHighException : public std::exception
	{
		public:
		  const char * what(void) const throw();
	};
	class GradeTooLowException : public std::exception
	{
		public:
		  const char * what(void) const throw();
	};
};


const char * Bureaucrat::GradeTooHighException::what(void) const throw()
{
	return "Grade too high...";
}

const char * Bureaucrat::GradeTooLowException::what(void) const throw()
{
	return "Grade too low...";
}

Bureaucrat::Bureaucrat(std::string name, int grade) : name(name)
{
	this->grade = grade;
	if (grade < 1)
	{
		throw Bureaucrat::GradeTooHighException();
	}
	else if (grade > 150)
	{
		throw Bureaucrat::GradeTooLowException();
	}
}

 

c++에서는 예외를 try…catch문과 throw 문을 이용하여 처리한다.

 

예외 상황을 미리 규정해두고 exception 클래스를 상속받은 클래스의 what() 함수의 return에 에러 내용을 적어두면 예외 상황이 발생했을 때 가장 가까운 try catch문에 해당 에러 내용을 던질 수 있다.

 

int main()
{
	try
	{
		try
		{
			Bureaucrat test("test", 200);	
		}
		catch(const std::exception& e)
		{
			std::cerr << e.what() << '\n';
		}
		
		Bureaucrat a("a", 150);
		Bureaucrat b("b", 1);

		std::cout << a << std::endl;
		std::cout << b << std::endl;
		std::cout << std::endl;
		
		// a.decrementGrade();
	
		// b.incrementGrade();
	
		//std::cout << a << std::endl;
		//std::cout << b << std::endl;
		//std::cout << std::endl;

		a.incrementGrade();
		b.decrementGrade();
		std::cout << a << std::endl;
		std::cout << b << std::endl;
		std::cout << std::endl;
	}
	catch(const std::exception& e)
	{
		std::cerr << e.what() << '\n';
	}
	
	return (0);
}

 

만약 위 코드에서 1~150을 벗어난 생성자를 생성할 때, 어디에도 try catch가 없다면 abort를 발생시키게 된다.

 

만약 메인에 try catch문이 있다면 throw 가 출력된 후 프로그램이 정지하겠지만, 만약 정지되는 상황이 다른 함수에서 try catch로 감싸 져 있다면 exception은 가장 가까운 catch까지만 전달되기 때문에 에러 상황을 출력한 후 나머지는 계속 진행된다.

 

따라서 일반적인 경우에 하나의 try catch문은 하나의 작업 단위(트랜잭션)에서 사용하게 된다. 만약 하나의 작업 단위에서 여러 개의 try catch문을 사용할 경우 종료해야 할 작업이 계속 진행되거나, 다른 작업의 예외처리를 하지 못하게 되므로 주의해야 한다.

 

try catch와 throw문을 사용한 방식의 가장 큰 장점은 에러 상황을 반환할 때 어떻게 반환해야 하는지를 고민하지 않아도 된다는 점이다. 만약 위 코드에서 try catch를 사용하지 않고 에러를 반환한다면 반환 값을 어떻게 넘겨서 처리해야 함수를 호출한 곳에서 함수 안에서 에러가 발생했는지를 알게 만들 수 있을지 감도 오지 않는데, throw 키워드를 사용하기만 하면 쉽게 처리할 수 있게 된다.

 

다른 장점은 스택 풀기(stack unwinding)가 자동으로 수행된다는 점이다. 만약 함수 안의 함수를 여러 차례 들어갈 때 예외 상황을 try catch를 사용하지 않고 처리한다면, 예외상황으로 지정한 반환 값을 처음 함수를 수행한 곳까지 계속 전달해야 하지만, 이 작업도 자동으로 수행해준다. 다만 이러한 작업은 스택 영역에서만 제대로 동작하기 때문에 포인터로 선언한 변수에 대해서는 메모리 릭에 유의해야 한다.

 

https://modoocode.com/230

 

씹어먹는 C ++ - <11. C++ 에서 예외 처리>

 

modoocode.com

https://ansohxxn.github.io/cpp/chapter14-3/

 

C++ Chapter 14.3 : 예외 클래스와 상속

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀 🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!

ansohxxn.github.io

https://ansohxxn.github.io/cpp/chapter14-4/

 

C++ Chapter 14.4 : std::exception

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀 🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!

ansohxxn.github.io

https://wikidocs.net/229

 

07-04 예외처리 (Exception)

프로그램을 만들다 보면 수없이 많은 오류가 발생한다. 물론 오류가 발생하는 이유는 프로그램이 오동작을 하지 않기 하기 위한 자바의 배려이다. 하지만 때로는 이러한 오류를 무 ...

wikidocs.net

https://musket-ade.tistory.com/entry/C-Stack-Unwinding스택-풀기

 

[ C++ ] Stack Unwinding(스택 풀기)

이전 포스팅에서 함수 내에서 throw를 던진 경우에 그 함수를 호출한 영역으로 돌아가서 예외처리를 하였다. 이렇듯, 예외처리에 대한 책임은 throw가 발생한 함수를 호출한 영역으로 넘어가게 된

musket-ade.tistory.com

 

 

ex01

관료들이 처리하는 Form을 만든다

 

상수 이름, sign 여부(bool), sign에 필요한 상수 grade, 실행하기에 필요한 상수 grade. 이 멤버 변수들은 private으로 만들어라.

 

관료에서 처리했던 것과 동일하게 등급이 너무 높거나 낮으면 예외로 처리하며, << 을 통해 콘솔에 출력하는 것도 구현하라.

 

관료를 인자로 받는 beSigned 멤버 함수를 만들어서 해당 관료의 등급이 이 Form이 요구하는 등급보다 높거나 같으면 sign 여부를 true로 변경한다. 낮다면 예외로 처리한다.

 

그리고 signForm() 함수를 관료의 멤버 함수로 추가하여 서명에 성공하거나 실패하면 해당 내용을 콘솔에 출력한다.

 

std::ostream& operator<<(std::ostream& out, const Form& f)
{
	out << f.getName() 
		<< std::boolalpha <<", signed : " << f.getSigned()
		<< ", signGrade : " << f.getSignGrade()
		<< ", execGrade : " << f.getExecGrade();
	return (out);
}

 

bool 자료형을 출력할 때 0과 1로만 출력될 텐데, 이걸 개선하기 위해서는 출력 앞에 std::boolalpha를 붙여주면 된다. 그러면 의도한 대로 true false로 출력이 된다.

 

https://woodforest.tistory.com/92

 

[C++] bool 출력 - boolalpha

bool a = false; bool b = true; 일 때 cout< cout< 하면 0 1 이 출력됩니다 true/false로 출력하고 싶을땐 if/else를 쓰기 귀찮기도 하죠 c++에서는 방법을 제공합니다 cout< cout< 하면 false true 로 출력됩니다.

woodforest.tistory.com

 

 

ex02

01에서 작성한 Form을 추상 클래스로 만들고, 그걸 상속받은 세 개의 클래스를 만들어서 실행할 수 있게 만들어라.(Form의 private 은 그대로 남겨둬야 한다.)

 

ShrubberyCreationForm - <target>_shrubbery 파일을 만들어서 그 안에 아스키 트리를 넣어라

RobotomyRequestForm - 소음이 출력되고, <target>의 로봇화가 50% 확률로 성공했음을 알려라. 아니면 실패했음을 알리거나.

PresidentialPardonForm - Zaphod Beeblebrox가 <target>을 사면시켰음을 알려라.

 

모든 클래스의 생성자는 인자를 target(name) 하나만 받는다.

 

Form에 execute(Bureocrat const & executor) const를 추가하고 양식이 서명되어있으며, 실행하는 관료의 등급이 높거나 같은지를 확인하고 예외처리를 해야 한다.

 

각각 자식 클래스에서 위의 예외처리를 구현할지, 아니면 기본 클래스에서 다른 함수를 호출하여 확인할지는 사용자에게 달려있는데, 어느 한쪽이 더 좋긴 하다.

 

마지막으로 관료에게 executeForm(Form const & form) 멤버 함수를 만들고, 실행 후에 적절한 출력을 해라.

 

class ThisIsNotSignedException : public std::exception
	{
		public:
		  const char * what(void) const throw();
	};
	virtual void execute(const Bureaucrat& b) const = 0;
	void setName(std::string name);
	void setSigned(bool b);
	void checkExec(const Bureaucrat& b) const;

 

Form 클래스의 execute를 순수 가상 함수로 만들어서 추상 클래스로 만들었다.

 

또한 Form의 private 멤버 변수들을 protected로 바꾸지 못하도록 되어있기 때문에, 변경이 필요한 멤버 변수를 변경하는 set 함수들을 만들어준다.

 

예외처리를 자식 클래스마다 따로 구현할지 아니면 Form에서 따로 함수를 만들어서 구현할 건지를 물어보는데, 후자가 멤버 변수에 접근하기가 편하고 중복으로 구현할 필요가 없을 것 같아서 그렇게 구현하였다.

 

class ShrubberyCreationForm : public Form
{
 private:
	ShrubberyCreationForm(void);

 public:
	ShrubberyCreationForm(const ShrubberyCreationForm& obj);
	ShrubberyCreationForm& operator=(const ShrubberyCreationForm& obj);
	~ShrubberyCreationForm(void);
	ShrubberyCreationForm(std::string target);
	void execute(const Bureaucrat& b) const;
};

 

그러고 나면 자식 클래스에서는 실행 부분만 구현을 해주면 된다. 다른 자식 클래스도 다 동일하게 생겼으니 생략.

 

ShrubberyCreationForm에서는 파일을 만들어서 그 파일에 일정한 문자열을 집어넣으면 된다. 파일을 만들거나 문자열을 만드는 과정은 01/ex04에서 이미 진행해봤을 테니 따로 설명하지는 않겠다.

 

안에 ASCII로 숲을 그려 넣으라길래 뭘 어떻게 해야 하나 감이 안 왔는데 그냥 구글에 ascii forest art라고 검색해서 나오는 적당한 문자열을 선택하면 된다.

 

std::string contents = 	
"                           %                         %            \n\
 @@@     %    @@     @@@@                                  *       \n\
    @@ %     @  %   @   %          %               ;      ***      \n\
     @@    @      @                               #     *****     \n\
 @@@   @@@@@  @@@@@@___    %         %           {###}  *******\n\
    @-@  @   @      @@@@       %               <## ####>********\n\
  @@@@@   @ @         @@@@@               %      {###}***********\n\
    %  @  @@ /@@@@@                          <###########> *******\n\
        @-@@@@    V     %          %           {#######}******* ***\n\
  %       @@      v        { }             <###############>*******\n\
          @@               {^^,               {## ######}***** ****\n\
  %      @@     %          (   `-;       <###################> ****\n\
         @@           _     `;;~~           {#############}********\n\
         @@   %      /(______);        <################  #####>***\n\
   %     @@@        (         (           {##################}*****\n\
          @@@        |:------( )     <##########################>**\n\
 @@@@     @@@      _//        \\        {###   ##############}*****\n\
@@@@@@@  @@@@@    / /@@@@@@@@@ vv  <##############################>\n\
@@@@@@@ @@@@@@@ @@@@@@@@@@@@@@@@@@@       @@@@@@  @@@@@@@  @@@@\n\
@@@@@@###@@@@@### @@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n\
@@@@@@@@###@##@@ @@@@@@@@@@@@@@@@@@@@@ @@@@@   @@@@@@@@@@@@@@@@@@@\n\
@@@@@@@@@@@### @@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@\n\
-@@@@@@@@@#####@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@";

 

하나의 문자열에 끝에 ‘\n\’를 넣어서 계속 그리면 여러 줄을 가진 문자열을 하나의 string에 넣을 수 있다. 다른 방법도 있는데, 나는 왜인지 몰라도 내 맥에서는 다른 방법이 안돼서 이렇게 그렸다.

 

RobotomyRequestForm에서는 50%의 확률로 기계화에 성공하거나 실패하는 상황을 구현해야 한다. 다만 c++의 random은 c++11이기 때문에 사용할 수 없고, C의 rand를 가져와야 한다.

 

int tmp = (rand() % 2); 

	std::cout << "drill say : drrrrrrrrrrrr...!" << std::endl;

	if (tmp)
	{
		std::cout << "wow! " << this->getName() << " succeeded in becoming a robot" << std::endl;
	}
	else
	{
		std::cout << "sorry... " << this->getName() << " failed to becoming a robot" << std::endl;
	}

 

사실 한참 고민한 게 로봇화에 실패한 상황이 exception인지 좀 헷갈렸는데, 다른 사람들에게 물어보니 문서는 제대로 실행이 되었으니 이 관료사회에서는 그것도 성공이지 않겠냐고 해서 듣고 보니 맞는 말이라 그렇게 구현했다. 이건 50%의 실패를 exception으로 구현했어도 이해할만한 부분인 것 같다.

 

void PresidentialPardonForm::execute(const Bureaucrat& b) const
{
	checkExec(b);
	
	std::cout << this->getName() << " was pardoned by President Zaphod Beeblebrox.." << std::endl;
}

 

PresidentialPardonForm에서는 따로 뭘 구현하지는 않고 그냥 문자열만 출력하도록 했다.

 

void Bureaucrat::signForm(Form& f) const
{
	try
	{
		f.beSigned(*this);
		std::cout << this->name << " signed " << f.getName() << std::endl;
	}
	catch(const std::exception& e)
	{
		std::cerr << this->name << " couldn't sign " << f.getName() << " because "<< e.what() << std::endl;
	}
}

 

관료 클래스에서 try catch문을 만들어서 메인 문에서의 코드가 복잡하거나 중간에 중단되지 않도록 구현하였다.

 

https://www.techiedelight.com/ko/create-a-multiline-string-literal-in-cpp/

 

C++에서 여러 줄 문자열 리터럴 만들기

이 게시물에서는 C++에서 여러 줄 문자열 리터럴을 만드는 방법에 대해 설명합니다. 1. 문자열 리터럴 사용 C++는 두 개 이상의 문자열 리터럴이 인접한 경우 컴파일러에 의해 함께 결합되는 암시

www.techiedelight.com

https://cplusplus.com/forum/beginner/79626/

 

How to do 50/50 chance of a number - C++ Forum

Sep 20, 2012 at 4:25pm UTC I am doing a Plinko program and when you drop a chip in, it has a 50/50 shot at either going left or right(which correspond to numbers such as 3.5 or 4). How would you make a 50/50 chance of something equal either a 3.5 or a 4? T

cplusplus.com

https://blog.naver.com/njw1204/221079839989

 

C언어/C++ rand 함수(랜덤 함수) 사용시 주의해야 할 점 (srand 함수 관련)

시작하기 앞서 C++ 레퍼런스의 rand 함수 정보를 보고 갑시다. rand 함수는 0이상 RAND_MAX이하의 ...

blog.naver.com

 

 

ex03

양식을 대신 만들어주는 인턴 클래스를 구현하라. 이름이나 성적 같은 잡다한 것은 필요 없고, makeForm()이라고 하는 멤버 함수를 가진다. 해당 함수의 인자로는 함수의 타입과 target의 이름을 가진다. 만약 양식 이름이 없다면 명시적 오류 메시지를 인쇄해야 한다.

 

흉한 if/elseif/else문의 범벅을 사용하지 않고 구현해야 한다.

 

Form* Intern::makeForm(std::string type, std::string name)
{
	std::string types[3] = {"shrubbery creation", "robotomy request", "presidential pardon"};

	for (int i = 0; i < 3; i++)
	{
		if (types[i] == type) 
		{
			return (f[i]->clone(name));
		}
	}
	throw Intern::NoTypeExcption();
	return (NULL);
}

 

01/ex06에서 switch를 사용해보라며 비슷한 주제로 구현을 해본 적이 있는데, 그때처럼 함수 포인터를 사용하면 for문 하나안에 if문 하나를 넣어서 깔끔하게 만들 수 있다.

 

switch를 사용해서 구현하는 사람도 있을 테고 나처럼 for 하나 if 하나 써서 구현하는 사람도 있을 텐데, 구현 자체는 전자가 훨씬 쉽지만 나중에 인턴에게 추가로 Form을 넣어준다고 했을 때의 작업량은 후자가 더 적을 거라고 생각해서 그렇게 구현하였다. 그런데 나중에 평가할 때 보니 함수 포인터를 사용했는지를 체크하고 있었다.