[C++] 예외처리와 템플릿
LastMod:
👨💻 개인 공부 기록용 블로그 입니다.
💡 틀린 내용이나 오타는 댓글, 메일로 제보해주시면 감사하겠습니다!! (__)
42 Cursus - Cpp Module을 진행하기 위해 정리했었던 C++98 기본 개념들로, C++11, C++14와 다른 내용이 있을 수 있습니다.
각 개념은 정리한 시간 순으로 배치되어 있으며, 한 포스트에 배치된 개념이 크게 관련이 없을 수 있습니다.
에외 처리 (Exception) 와 try-catch, throw
- C에서는 오류는 거의 메모리 문제로 귀결 되었다. (seg fault, heap buffer overflow 등)
- 그 외 기타 사용자 오류는 모두 if-else 조건문 분기를 통해 리턴값을 다르게 주는 형태로 처리하였다.
- C++에서는
std::exception
를 상속받은 클래스들을throw
함으로써 각종 예외를 처리할 수 있다. try
블록 안에서throw
된exception
은catch
구문을 통해 받을 수 있으며, 받은exception
의 종류에 따라 다양한 처리를 해줄 수 있다.
throw expression
throw "expression"
- expression 자리는 예외 객체가 들어가며, throw 표현식에 의해 생성되는 임시 객체이다.
- throw 표현식은 가장 가까운 catch로 흐름을 점프시킨다. (throw 밑에 있는 문장을 실행시키지 않는다.)
- 따라서 소멸자만 제대로 작성하였다면 예외가 발생하여도 사용하고 있는 자원들을 제대로 소멸시킬 수 있다.
try-catch block
try { /* */ } catch (const std::exception& e) { /* */ }
- 예외가 발생할 수 있는 코드를
try
블록 안에 넣는다. - 만약
try
블록 안에서 예외가 발생한다면 (throw
표현식이 작동한다면), 제일 가까운catch
가 해당 오류를 받아서catch
블록 안에 있는 코드를 실행시킨다. - 예외가 발생하지 않는다면
catch
블록을 무시한다. - catch 구문의 매개변수가 레퍼런스인 이유는 해당 객체에 대한 모든 변경사항을 반영하여, exception이 또 다른 exception을 던질 때에도 다른 핸들러에서 관찰할 수 있게 하기 위함이다.
- 만약 레퍼런스를 사용하지 않는다면, 모든 변경사항이 로컬에 저장 되어 해당 핸들러가 종료되자 마자 예외 객체가 사라진다.
- 메인 try-catch의 try 블록에서 함수 호출 → 해당 함수 내에서도 try-catch 블록이 존재하며, try 블록 내에서 예외 발생 → catch 내에서 처리 후 다시 throw → 메인 try-catch에서도 해당 에러 객체를 받아서 처리 가능
try
{
f();
}
catch (const std::overflow_error& e)
{} // this executes if f() throws std::overflow_error (same type rule)
catch (const std::runtime_error& e)
{} // this executes if f() throws std::underflow_error (base class rule)
catch (const std::exception& e)
{} // this executes if f() throws std::logic_error (base class rule)
catch (...)
{} // this executes if f() throws std::string or int or any other unrelated type
- 위와 같이 여러 종류의 exception을 받을 수 있다.
- try-catch 구문 내에서 switch 분기를 사용하거나 goto를 사용하면 안된다.
std::exception
-
what()
이라는 가상함수를 가진다.virtual const char* what() const throw(); // until C++11 virtual const char* what() const noexcept; // since C++11
- 함수 형태에서 볼 수 있다시피, 에러를 던지면서 출력해줄 문자열을 C스타일로 반환해야 한다.
- 다양한 표준 에러 객체는 레퍼런스를 참고하자.
User Defined Exception
- 프로그래머가 직접 예외를 정의할 때에는 위의 std::exception을 public 상속하여 클래스로 구현한다.
- 반드시 public 상속해야 하며, 그렇지 않으면 런타임 에러가 발생한다. → terminating due to uncaught exception
// 구현하고자 하는 클래스 안에서
class MyException : public std::exception
{
public:
const char * what(void) const throw();
};
// 뒤에 붙는 throw()는 하나의 키워드로써, 예외가 발생하지 않는 멤버 변수라는 것을 컴파일러에게 알려주는 역할을한다.
// C++11부터는 throw()대신 noexcept 사용을 권장한다. (키워드로써 붙는 throw()는 deprecated)
스택 되감기 (Stack unwinding)
- 일반적으로 함수를 호출할 때 Call Stack에 함수의 주소를 저장하고, 함수가 종료되었을 때 Call Stack에서 pop한다.
- Stack unwinding은 try-catch문에서 함수 호출 중 예외가 발생하였을 때, Call Stack에 저장되었던 함수들의 주소가 제거되어 최초의 try-catch를 호출한 함수로 돌아가는 것을 말한다.
템플릿 (Template)
- C++ 에서는 C에서와 달리, 동일한 함수 이름을 가지고 다양한 타입으로 재정의할 수 있다.
- 하지만, 타입마다 재정의를 해줘야 하는 단점이 있는데, 이를 해결하기 위해 도입된 것이 템플릿 (Template)이다.
template <typename T>
와 같이 사용하고, 그 밑에 T에 대한 함수, 클래스를 정의하면 된다. 종류에 따라 클래스 템플릿, 함수 템플릿으로 나뉜다.- 템플릿은 인스턴스화 되기 전까지, 컴파일 타임에는 그냥 코드로 남아있다.
클래스 템플릿 (Class Templates)
- 템플릿을 클래스에 대해 사용하는 것을 클래스 템플릿 (Class Templates) 이라고 한다.
- 예를 들어, STL 컨테이너의 대표적인 한 종류인 벡터 (Vector)에 대해 생각할 수 있는데, 자료형에 따라 매번 벡터를 재정의할 수 없기 때문에 템플릿을 통해 구현되었다.
template <typename T>
class Vector {
T* data;
int capacity;
// ...
- 위에서 말했다시피, 컴파일 타임에는 그냥 코드로 남아있다가 해당 클래스가 인스턴스화 된다면,
T
가 인자로 전달된 타입으로 모두 치환된다. - 하지만 특정
T
에 대해서 다른 처리를 해주고 싶을 수 있는데, 그 경우는 밑에 있는 템플릿 특수화에서 알아보자.
템플릿 특수화 (Templates Specialization)
- 특정 타입에 대해 템플릿을 다르게 처리해주는 것을 템플릿 특수화 (Templates Specialization) 라고 한다.
- 템플릿 특수화를 위해서는 아래와 같이 작성한다.
// 예를 들어, bool 타입의 vector를 따로 처리해주고 싶다면,
template <>
class Vector<bool> {
// ...
// 와 같이 사용한다.
// 물론 ... 에는 T를 사용할 수 없다.
// 아래와 같이 여러 typename을 받는 클래스인 test에 대해서
template <typename A, typename B, typename C>
class test {};
// B 빼고 나머지 타입을 특정시키고 싶다면 아래와 같이 사용한다.
template <typename B>
class test<int, B, double> {};
함수 템플릿 (Function Templates)
- 함수 템플릿 또한 위 특성을 모두 공유한다.
template <typename T>
T max(T& a, T& b) {
return a > b ? a : b;
}
함수 객체 (Functor; Function Object)
- 파이썬에서 정렬 시
key
함수를 따로 지정해주는 것 처럼, 객체 간 비교 등에 일반적이지 않은 다른 비교연산 사용하고 싶을 수 있다. - 물론 사용자 객체의 경우 연산자를 오버로딩 하여 구현하면 그만이지만, 문제는 기존에 구현된 객체 (
int
vs.string
) 와 같은 경우는 그럴 수 없다는 것이다. - 아래와 같이 구현하여 사용할 수 있다.
struct Comp2 {
bool operator()(int a, int b) { return a < b; }
};
Comp1 comp1;
std::sort(vec.begin(), vec.end(), comp1);
Leave a comment