본문 바로가기

CS/자료구조

[ 자료구조 ] 템플릿, 예외처리

반응형

템플릿

공식문서에 따르면, '템플릿은 사용자가 템플릿 매개 변수에 대해 제공하는 인수에 따라 컴파일 시간에 일반 형식 또는 함수를 생성하는 구문' 이라고 나와있다. 그냥 변수 타입이 컴파일 시간에 결정된다고 이해하면 될 것 같다. 사실 우리는 템플릿을 자주 사용해왔다. C++에서 제공하는 백터나 큐, 스택을 사용할때 <> 안에 변수 타입을 적었을 것이다. 이게 바로 템플릿이다. 

 

템플릿은 함수와 클래스에 모두 적용 가능하다. 기본 형식은 아래와 같다. 

 

template<typename T>
// 함수 또는 클래스 정의

 

T는 모든 타입을 포함하는 대단한 녀석이다. 당연히 T 말고 다른 문자를 써도 된다. 먼저 함수 템플릿을 살펴보자.

 

// 함수 템플릿

// 벡터 안에 있는 모든 값의 합을 반환
template<typename T> 
T sum(const vector<T>& v) {
    T result = 0;
    for (T elem : v)
        result += elem;
    return result;
}

// x < y 임을 반환
template<typename T, typename U>
bool less_than(const T& x,const U& y) { 
    return x < y; 
}

int main() {
    vector<int> v{ 1,2,3 };
    vector<double> v2{ 1.1,2.2,3.3 };
    cout << sum(v) << endl;
    cout << sum(v2) << endl; 
    
    cout << less_than(2, 3) << endl;
    cout << less_than(2.3, 2.7) << endl;
    cout << less_than(2, 2.7) << endl;

    return 0;
}

 

클래스 탬플릿 사용법은 아래와 같다. 멤버함수가 외부에서 선언될 때의 문법이 좀 특이한데 알아두자! 또 객체를 생성할 때 T를 명시해줘야 한다. 

 

template<class T>
class Point {
private:
	T x;
	T y;
public:
	Point(T _x, T _y);
	T getX() const;
	T getY() const;
};

// class가 외부 선언될 때의 문법
template<class T> 
Point<T>::Point(T _x, T _y) :x(_x), y(_y) {}

template<class T>
T Point<T>::getX() const { return x; }

template<class T>
T Point<T>::getY() const { return y; }

int main() {
	Point<int> pt1(1, 2); // T가 int로 mapping
	Point<double> pt2(1.1, 2.2); // T가 double로 mapping
    return 0;
}

 

템플릿은 이정도만 알고 넘어가도 상관없다. 바로 예외처리로 넘어가자. 

예외처리

예외처리는 대부분의 언어에서 지원하고 있다. try, catch문에 대해 한 번쯤은 들어봤을거라고 생각한다. 사실 단순 조건문으로 예외처리를 해도 상관없지만, 예외처리문을 사용하면 가독성이 훨씬 높아지기 때문에 사용하는 것이 좋다. 예외는 던져지는 '객체'로 볼 수 있다. 책에서는 아래와 같이 기술하고 있다. 

 

C++에서 예외는 예기치 못한 상황에 마주쳤을 때 처리할 수 있도록 만들어진 코드에 의해서 "던져지는(thrown)" 객체이다. 

 

여기서 키워드는 '던져지는'과 '객체다'. 에러가 발생하면 객체를 던지는데, 이 객체는 던져지는 곳과 가장 가까운 catch문에서 잡힌다.  그러면 catch문에서 오류를 처리하고, catch문 블록 바로 다음줄부터 다시 실행된다.

 

오류는 '객체'이기 때문에 클래스로 정의될 수 있다. 일반적으로 오류 타입을 대표하는 부모 클래스를 하나 만들고, 그 밑에 세부적인 예외 클래스를 정의한다. 수학적 예외를 나타내는 'MathException'이라는 클래스를 정의해보자. 

 

class MathException{ // 일반 수학적 예외
private:
    string errMsg;
public:
    MathException(const string& err):errMsg(err) {}
    string getError() { return errMsg; }
};

 

그다음에 0으로 나누는 오류를 처리하기 위한 ZeroDivide 와 음수 제곱근을 계산하려는 오류를 처리하기 위한 NegativeRoot같은 특정한 예외를 추가한다. 

 

// 0으로 나누는 예외
class ZeroDivide : MathException{
public:
    ZeroDivide(const string& err):MathException(err) {}
};

// 음의 제곱근 예외
class NegativeRoot : MathException{
public:
    NegativeRoot(const string& err):MathException(err) {}
};

 

 

서로 상속 관계에 있기 때문에 다형성이 적용 가능하다. 부모가 자식을 대신할 수 있기 때문에 catch문에서 '객체 잘림' 현상이 발생할 수 있는데 이는 곧 나오니깐 우선 넘어가자. 이제 위 예외처리 객체를 통해 어떤식으로 예외처리를 하는지 살펴보자. 

 

먼저 예외처리의 큰 틀은 아래와 같다. 

 

try{
    // ...
    if(...){
        throw 예외이름(매개변수1, 매개변수2, ...) // 생성자
    }
}
catch(예외이름 식별자){
    catch 명령문 1
}
catch(예외이름 식별자){
    catch 명령문 2
}

 

 

throw의 형태를 잘 보면 생성자임을 알 수 있다. 아까 위에서 예외는 던져지는 '객체'라고 했는데, 생성자를 던지는 것이다. try 블록 안에서 예외가 발생하면 그 지점에서 실행어가 끝나고, 던져진 예외와 매칭되는 첫 번째 catch 블록으로 점프한다. catch문의 '식별자'는 예외 객체 그 자체를 식별한다. 식별자를 통해 예외객체의 멤버함수에 접근할 수 있다. catch 블록의 실행이 끝나면 마지막 catch 블록 이후의 첫 번째 명령어로 점프한다. 아래는 예외처리 예시다.

 

try{
    // ...
    if(divisor == 0)
        throw ZeroDivide("Divide by zero in Module X");
}
catch(ZeroDivide& zde){
    // 0으로 나누는 것을 처리
}
catch(MathException& me){
    // 0으로 나누는 것과 다른 산술 예외 처리
}

 

여기서 MathException 과 ZeroDivide는 서로 상속 관계이기 때문에 catch문의 순서가 중요하다. 만약에 MathException 이 위로 가면 어떻게 될까? 아까 throw 하면 던져진 예외와 매칭되는 첫 번째 catch블록으로 점프한다고 했었다. 그런데 여기서 다형성이 적용되기 때문에 ZeroDivide가 던져지면 MathException에 걸리게 된다. 이를 '객체 잘림 현상'이라고 한다.

 

이를 방지하기 위해서는 catch 문의 순서를 바꿔주는 방법과 가상함수를 정의해주는 방법이 있다. 가장 포괄적인 예외를 가장 아래의 catch문에 위치시키거나, 예외객체의 멤버함수에 'virtual' 키워드를 넣어주면 된다. 

 

 

 

마지막으로 크게 중요한건 아닌데... 함수를 선언할 때 발생할 수 있는 예외들을 지정할 수 있는 문법이 있다. 형식은 아래와 같다. 

 

void function() throw(예외1, 예외2, ...){
    // 함수 본체
}


// 예시
void calculator() throw(ZeroDivide, NegativeRoot){
    // ...
    throw ZeroDivide("zerodivide error");
    
    // ...
    throw NegativeRoot("negativeroot error");
    
    // ...
}

 

 

그런데 보통 함수를 정의할때 발생할 수 있는 모든 예외를 적지는 않는다. 사실 나도 이때까지 예외를 명시하는 함수를 본 적 없다. 그렇다면 우리가 흔히 사용하는 throw() 가 생략된 함수는 무엇을 의미할까? 어떤 예외도 던질 수 있음을 의미한다. 아무런 예외도 던질 수 없는 경우는 throw()를 적어줘야 한다. 뭔가 쓸 일이 없을거 같다. (추가) 새로 알게된 사실인데 이 기능은 C++ 17 에서는 지원하지 않는다.

 

void func1(); // 어떤 예외도 던질 수 있다
void func2() throw(); // 아무런 예외도 던질 수 없다

 

 

오늘은 여기까지! 다음 시간부터가 본격적인 자료구조 시간이다. 리스트부터 시작해보자.

 

 

반응형