Friend Class

Nested Class

Exception class

프로그램은 runtime problem을 마주치곤 한다. 존재하지 않는 파일에 접근이나, 메모리의 부족이 가장 자주 발생한다. 이러한 상황에 대처하는 강력하고 유연한 exception들을 c++은 지원한다.

Calling abort()

먼저 문제가 발생하면 프로그램을 종료하는 방법을 알아보겠다.

#include <cstdlib>
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while(std::cin >> x >> y) {
        z = hmean(x,y);
        std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    return 0;
}
double hmean(double a, double b) {
    if(a == -b) {
        std::cout << "Untenable arguments to hmean()\n";
        std::abort();
    }
    return 2.0 * a * b / (a + b);
}

위의 [error-abort.cpp]는 standard error steam에 에러메세지를 보내고, 프로그램을 종료한다.

Returning an Error Code

매번 프로그램을 종료시키는 것보다 조금 더 유연한 방식은 예외상황이 발생했는지 표시하는 return value를 반환하는 것이다.

bool hmean(double a, double b, double* ans)
{
    if(a == - b) {
        *ans = DBL_MAX;
        return false;
    }
    else {
        *ans = 2.0 * a * b / (a + b);
        return true;
    }
}

실제로 [error-return.cpp]와 같은 방식은 자주 쓰인다. 예를 들어, istream::get(void)는 end-of-file에 도달하면 special value인 EOF를 반환한다. 위의 error1.cpp의 while 조건문도 비슷한 접근이다.

Exception Mechanism

프로그램 동작 중 예외적인 상황에 대한 대응으로 실행시킬 블락을 전이시킨다. try-catch-throw의 세 요소로 구성된다. try block 안에서 예외가 발생한다면 catch block이 대신 실행된다.

int main()
{
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while(std::cin >> x >> y) {
        try { // start of try block
            z = hmean(x,y);
        } // end of try block
        catch(const char* s) { // start of exception handler
            std::cout << s << std::endl;
            std::cout << "Enter a new pair of numbers: ";
            continue;
        } // end of handler
        std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b)
{
    if(a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b);
}

위의 [error-try-catch.cpp]는 hmean 함수를 실행하면서 문제가 생기면 exception handler(catch block)을 대신 실행시킨다. throw는 return처럼 함수 실행을 종료하며, char * s에 문자열을 반환한다.

Using Objects as Exceptions

위의 코드에서는 문자열을 throw했지만 일반적으로는 객체를 throw한다. 예외가 발생한 상황과 함수에 따라 다른 exception type을 사용하며 정보를 전달할 수 있기 때문이다.

class bad_hmean {
private:
    double v1;
    double v2;
public:
    bad_hmean(double a = 0., double b =0.) : v1(a), v2(b) { }
    void mesg() {
        std::cout << "hmean(" << v1 << ", " << v2 << "): " << "invalid arguments: a = -b\n";
    }
};
class bad_gmean
{
public:
    double v1;
    double v2;
    bad_gmean(double a = 0., double b =0.) : v1(a), v2(b) {}
    const char* mesg() {
        return "gmean() arguments should be >= 0\n";
    }
};

double hmean(double a, double b)
{
    if(a == -b) throw bad_hmean(a,b);
    return 2.0 * a * b / (a + b);
}
double gmean(double a, double b)
{
    if(a < 0 || b < 0) throw bad_gmean(a,b);
    return std::sqrt(a * b);
}

int main() {
    double x, y;
    cout << "Enter two numbers: ";
    while(cin >> x >> y) {
        try { // start of try block
            cout << "Harmonic mean is " << hmean(x,y) << endl;
            cout << "Geometric mean is " << gmean(x,y) << endl;
        } // end of try block
        catch(bad_hmean& bg) { // start of catch block
            bg.mesg();
            cout << "Try again.\n";
            continue;
        }
        catch(bad_gmean& hg) {
            cout << hg.mesg();
            cout << "Values used: " << hg.v1 << ", " << hg.v2 << endl;
            cout << "Sorry, you don't get to play any more.\n";
            break;
        } // end of catch block
    }
    cout << "Bye!\n";
    return 0;
}

[error-throw-object]는 어떤 상황에서 문제가 발생했는지에 따라 다른 catch문과 exception class을 사용하여 정보를 전달할 수 있다.

  • Unwinding the Stack : exception이 발생하여 throw에 도달하면, function call stack들은 try block에 도달할 때까지 자동으로 제거된다. 이를 unwinding the stack이라고 부른다. 만약 main에도 try가 없었다면 프로그램이 종료된다.

  • Rethrowing an exception : exception handler 내에서 throw; 가 쓰이면 catch한 exception을 call stack의 low level로 그대로 보낸다.

  • catch(…) : 특정 type인지 모르더라도 예외를 catch할 수 있다. 마치 switch문의 default와 같은 역할을 한다. 그러나 catch문은 sequential하게 처리되기 때문에 default exception은 반드시 맨 뒤에 있어야 한다.

exception Class

c++을 사용자가 정의한 예외처리 클래스들의 base class로 사용할 수 있도록 exception class를 제공한다.

#include<exception>
class bad_hmean : public std::exception
{
public:
    const char* what() { return "bad arguments to hmean()"; }
};
class bad_gmean : public std::exception
{
public:
    const char* what() { return "bad arguments to gmean()"; }
}

try {
    // ...
}
catch(std::exception& e) {  // Polymorphism!
    cout << e.what() << endl;
}

위의 예시 코드에서 birtual member function인 what()은 string을 반환하며, 사용자에 의해 재정의 될 수 있다. 메모리를 할당하면서 발생하는 에러를 위와 같은 방식으로 예외처리한 [bad_alloc.cpp]에서 주의해야할 것은 할당한 동적 메모리를 해제하는 것이다. throw한 함수 내에서 바로 catch하여 해제할 필요가 있다.

void test2(int n)
{
    double* ar = new double[n];
    // ...
    if(oh_no) throw exception();
    // ...
    delete[] ar;    // never happens if exception occurs
    return;
}
void test3(int n)
{
    double* ar = new double[n];
    try {
        if(oh_no) throw exception();    // throw exception
    }
    catch(exception& ex) {              // and catch it
        delete[] ar;                    // to deallocate
        throw;                          // rethrowing ex
    }
    // ...
    delete[] ar;
    return;
}
  • stdexcept Class : stdexcept 헤더파일은 더 많은 exception class들을 정의한다. logic_error과 runtime_error가 exception으로부터 상속 받아 정의되어 있다.

Runtime Type Identification(RTTI)

구 컴파일러에서는 지원하지 않을 수 있다.

dynamic_cast Operator

아래 코드는 아무런 compile error도 발생시키지 않는다. 과연 안전한 코드일까?

// Suppose a class hierarchy
class Base { ... };
class Derived1 : public Base { ... };
class Derived11 : public Derived1 { ... };
// Suppose the following pointers
Base* pb = new Base;
Base* pd1 = new Derived1;
Base* pd11 = new Derived11;
// Consider the following type casts
Derived11* p1 = (Derived11*)pd11;   // 11->11
Derived11* p2 = (Derived11*)pb;     // base->11
Derived1* p3 = (Derived1*)pd11;     // 11->1

절대 아니다. 첫번째는 동일한 type이므로 안전하다. 세번째는 base class pointer로 derived class를 가리킬 수 있으니 괜찮다. 그러나 두번째는 runtime에서 오류가 발생하게 된다.

이러한 상황을 막기 위해 dynamic_cast 연산자를 사용한다. Typecast가 가능하다면 그대로 바꿔주고, 그렇지 않다면 0(null-pointer)를 반환한다.

Derived derived_obj;
Base base_obj;

Base *base_ptr_do = &derived_obj;
Base *base_ptr_bo = &base_obj;

Derived* dp_bp_do = dynamic_cast<Derived*>(base_ptr_do); // allowed
Derived* dp_bp_bo = dynamic_cast<Derived*>(base_ptr_bo); // return nullptr

dynamic_cast는 reference에도 사용할 수 있다. 다만 null-pointer에 해당하는 type이 reference value로 없기 때문에 0 대신 bad_cast를 반환한다.

#include <typeinfo> // for bad_cast
try {
    Derived1& rs = dynamic_cast<Derived1&>(rb);
    // ...
}
catch(bad_cast&) {
    // ...
}

typeid operator

type_info structure

Questions?

Q1.
A1.

Q2.
A2.

[error-abort.cpp]: [error-return.cpp]: [error-try-catch]: [error-throw-object]: [bad-alloc.cpp]: