A tour of C++ 3장(ii)

A tour of C++ 3장(ii)

이번 3장(ii) 에서는 에러처리 및 에러처리, 계약, Call by value and reference, 구조화된 바인딩에 대해서 알아보고자 한다.

에러 처리

에러 처리는 언어의 기능은 물론 프로그래밍 기법과 도구까지 아우르는 방대하고 복잡한 주제이다. C++에서는 이를 돕는 몇 가지 기능을 제공한다.

C++ 에서 제공하는 에러처리

  1. 적절한 타입
    • string, map, regex 등 새로운 타입을 통한 에러처리를 함
  2. 알고리즘
    • sort(), find_if(), draw_all() 등과 같은 알고리즘을 통해 사전에 에러를 방지하도록 한다.

이러한 하이레벨에서의 에러처리 요소로 프로그래밍이 간단해지고 실수를 줄인다. 또한, 사전에 컴파일러를 통한 에러감지가 가능해진다. 프로그램의 규모가 커질수록, 특히 라이브러리를 많이 사용할 경우 에러를 처리하는 표준이 중요해진다. 따라서 프로그램 개발 초기에 에러 처리 전략을 고려하는 것이 좋다.

에러처리를 할 수 있는 방법들을 보자.

예외

에러처리 관련해 가장 먼저 떠올릴 수 있는 것은 “예외”이다. 에러가 발생할 경우를 예외적인 경우로 간주하고 사전에 알려준다. 우리가 만든 Vector를 가지고 예를 들어보자.

만약 Vector에서 스코프 밖의 요소에 접근하고자 한다면 어떻게 해야할까?

위와 같은 경우를 고려하고 프로그램을 만들어야 하는데, 위의 설명과 같이 우리는 사용자의 Vector 사용 유무를 비롯해 값이 어떤 것이 들어오는지 알 수 없다. 즉, 스코프 범위 밖의 요소에 접근한 이후 처리를 해야한다는 것인데, 이 경우에는 접근 시도가 이뤄진 후 잘 못을 판단하고 사용자에게 알려주는게 좋은 방법이다. 사용자는 잘못 된 것을 발견하고 후속 처리를 통해 해결하면 된다.

double& Vector::operator[] (int i)
{
	if (i < 0 || size() <= i)
		throw out_of_range("Vector::operator[]");

	return elem[i];
}

위의 index 연산자 함수를 보자. [] 연산자를 이용해 값에 접근할 때 index의 값이 음수거나 해당 Vector의 크기보다 큰 index를 요청할 경우 예외로 간주한다. 이후 throw를 통해 예외 처리 핸들러가 있는 곳에 예외가 발생했다는 것을 알려준다. standard error (#include )로 정의된 out_of_range 같은 경우 현재 scope를 넘어갔다는 error event이다. throw 같은 경우 함수의 호출 스택을 거슬러 예외를 처리할 수 있는 핸들러 까지 올라가게 된다. 즉, []연산과 관련된 함수들은 계속해서 throw로 넘기던지 아니면 handling을 해야한다.

그렇다면 error handling은 어떻게 하는지 보자.

void foo(Vector v) {
	// ...

	try {
		v[v.size()] = 8; // vector의 범위를 넘어선 접근 시도
	} catch (out_of_range& err) {
		cerr << err.what() << endl;
	}
	//...
}

예외 처리가 필요한 코드들은 try 블록으로 작성한다. 보통 throw를 이용해 예외처리를 하고 있는 연산들은 try로 처리하거나 throw 연산을 통해 더 위에서 에러처리하도록 넘긴다. 우리는 예외를 처리하는 부분에 집중해서 보자.

“try” 문은 말 그대로 해당 연산을 시도한다는 의미로, 해당 연산에서는 예외가 발생해 에러가 발생할 수 있다라는 의미를 내포한다. 위의 코드와 같이 Vector의 scope를 벗어나는 행위를 할 경우 error가 유발되고 catch 문에서 해당 error를 받아 처리하도록 한다. 즉, “catch” 문에서는 에러를 전달받아 핸들링하는 역할을 한다. 에러를 잡을 때, 복사를 피하기 위해 보통 참조로 전달받는다. (복사 관련된 이야기는 해당 포스트 마지막 부분에서 정리하도록 하겠다.)

what()는 throw를 사용한 시점에 설정된 메세지 즉, throw out_of_range(“Vector::operator[]”); 를 출력한다.

예외처리 메커니즘을 통한 error 처리는 간단하고 체계적이며 가독성 높은 에러 처리가 가능하다. 하지만, try구문을 너무 많이 사용하면 안된다. try구문이 코드에 너무 많이 삽입이 되게 되면 코드를 읽을 때 가독성이 떨어진다. 특히, 자원을 획득하는 경우 예외가 많이 발생할 수 있다. (memory 부족 등 문제) 빈번하게 발생하는 자원 할당 같은 경우 try 문으로 도배 될 경우 코드가 try문으로 가득해질 것이다. 따라서 C++에서는 RAII 디자인 패턴을 채용해 사용한다. 추후 RAII에 대해 정리를 할 것이니 그때 보도록 하자.

예시와 같이 스코프 밖 접근에 대한 신호로 예외를 사용하는 방식은 함수의 인자를 체크하고 기본적인 가정 즉, 사전 조건이 성립되지 않으면 작동을 거부하는 방식의 한 예로 볼 수 있다.

Vector 인덱싱 연산을 할 경우 인덱스는 반드시 [0:size()) 구간에 있어야 하는데, operator 에서 해단 구간을 검사했다. 해당 구간은 [a:b) 반개 구간을 말하며, a 구간은 포함되지만 b 구간에는 포함되지 않음을 의미한다.

함수를 정의할 떄는 사전 조건이 무엇이고, 그 조건을 체크하는지 고려해야한다.

추가적으로, 절대로 예외를 던지지 않을 경우 noexcept로 선언할 수 있다. 만약 noexcept로 선언된 코드에서 예외가 발생할 경우 terminate()가 호출돼 프로그램을 즉시 종료한다. noexcept 같은 경우 compile 타임에 해당 코드가 예외가 있는지 검사해 버그 유발을 줄일 수 있다. (이 부분은 추후 정리하도록 하겠다)

불변 조건

예외처리를 하기 전에 간단한 불변 조건을 확인하는 것이 도움이 된다. Operator[] 는 Vector 타입의 객체에 대해 작동한다. 하지만, Vector의 멤버가 적절한 값을 갖지 않는다면 무의미 해진다.

Vecto(-27);