{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] The C++ Programming Language: 2. A Tour of C++: The Basics (2)

 2.4.3 Error Handling

  에러 핸들링은 언어 기능의 한참 너머에, 프로그래밍 기법과 도구들까지 영향을 줄 수 있는 거대하고 복잡한 주제입니다. 하지만 C++는 이를 도울 수 있는 몇 가지 기능들을 제공하죠. 자료형 체계도 역시 그 스스로의 역할을 하고 있습니다. 내장 자료형과 구문들을 이용해서 힘겹게 응용프로그램을 작성하는 것보다는, 더 많은 자료형들과 알고리즘들을 적절히 개발하는 것이 더 좋죠. 

  이는 우리의 프로그래밍을 단순화시킵니다. 실수할 여지를 줄여주죠. 또한 컴파일러가 에러들을 잡아낼 기회 역시 부여합니다. 대부분의 C++ 구조는 효율적인 추상화와 우아한 구현 및 설계에 집중합니다. 이러한 모듈화와 추상화의 효과 중 하나는 런타임 에러를 감지하는 지점과 이를 처리하는 지점을 분리할 수 있다는 점입니다. 프로그램이 커질수록, 라이브러리가 확장될수록, 에러를 처리하기 위한 표준은 중요해집니다.

  

2.4.3.1 Exceptions

  Vector 예제를 다시 떠올려보죠. 2.3.2절의 벡터 범위를 넘어가는(out-of-range) 요소에 접근하려고 할 때 우리가 어떻게 해야할까요?

  • Vector의 작성자는 이 경우에 사용자가 어떠한 것을 하려고 하는 것인지 알 수 없습니다. (심지어, 어떤 프로그램에서 vector가 실행되고 있는지도 알 수 없죠)
  • Vector의 사용자는 지속적으로 문제를 감지할 수 없습니다. (만약 가능했다면, 애초에 범위를 벗어나는 접근을 하지도 않았겠죠. 문제가 어디서 발생하는지 알기 어렵다는 것입니다.-옮긴 이)

  이 문제의 해결책은 Vector 구현자가 out-of-range 접근을 시도했을 때 이를 감지하고 사용자에게 말해주는 것입니다. 사용자는 이를 가로채 적절히 행위할 수 있죠. 예를 들어, Vector::operator[]()는 out-of-range 접근을 시도했을 때 이를 감지하고 out_of_range 예외를 던집니다.

1
2
3
4
5
double& Vector::operator[](int i)
{
    if (i<0 || size()<=i) throw out_of_range{"Vector::operator[]"};
    return elem[i];
}
cs


  이곳에서 throw는 out_of_range 유형의 예외를 핸들러에게 전달합니다. 이는 Vector::operator[]()를 호출했을 때 발생할 수 있죠. 따라서 구현은 함수 호출 스택을 거슬러 올라가 호출자에게 돌아갈 것입니다. 다음을 참고하세요.

1
2
3
4
5
6
7
8
9
10
11
void f(Vector& v)
{
    // ...
    try { // exceptions here are handled by the handler defined below
        v[v.size()] = 7// try to access beyond the end of v
    }
    catch (out_of_range) { // oops: out_of_range error
        // ... handle range error ...
    }
    // ...
}
cs


  우리는 try-블록으로 예외 처리에 관심이 있는 코드를 넣을 수 있습니다. 또한 위에서 v[v.size()]는 실패할 것입니다. 따라서, catch절로 out_of_range 예외가 발생했을 때의 처리를 제공할 수 있죠. out_of_range 타입은 표준 라이브러리에 정의되어 있으며, 실제로도 몇몇 표준 라이브러리 컨테이너의 접근 함수에 사용되고 있습니다.

  예외 처리 메커니즘을 사용하면 에러 처리를 간단하고, 가독성 있고, 체계적으로 할 수 있습니다.


2.4.3.2 Invariants

  out-of-range 접근임을 알려주기 위해 쓰이는 예외는 함수의 인자를 확인하고 기본 가정, 곧 사전조건(precondition)에 맞지 않으면 거부하는 예시입니다. 벡터의 첨자 연산자를 생각해보죠. 우리는 다음과 같이 말했을 것입니다. "인덱스는 반드시 [0:size())의 범위에 있어야 한다." 우리가 함수를 정의할 때에는, 이것의 사전조건이 무엇이고 이를 검증할 수 있는지 고려해야 합니다.

  또한, operator[]()는 Vector 유형의 객체에 대해 작동하며 Vector의 구성원이 합리적인 경우가 아니면 아무 의미가 없습니다. 특히 우리는 "elem이 sz double의 배열을 가리킵니다"라고 말했지만 주석에서만 그렇게 말했습니다. 클래스에 대해 참이라고 가정되는 이러한 진술을 클래스 불변(class invariants) 또는 단순히 불변(invariants)이라고 합니다. 클래스에 대한 불변을 설정하고(멤버 함수가 이에 의존할 수 있도록) 멤버 함수가 종료할 때 해당 불변이 유지되도록 하는 것은 생성자의 작업입니다. 불행히도 Vector 생성자는 부분적으로만 작업을 수행했습니다. Vector 멤버를 올바르게 초기화했지만 전달된 인수가 의미가 있는지 확인하지 못했습니다. 가령:

1
Vector v(-27);
cs

  이런 코드는 문제를 야기하죠. 따라서 좀 더 적합한 정의를 봅시다:

1
2
3
4
5
6
Vector::Vector(int s)
{
    if (s<0) throw length_error{};
    elem = new double[s];
    sz = s;
}
cs

  저는 표준 라이브러리 예외인 length_error를 이용해 문제를 보고했습니다. 몇몇 표준 라이브러리 연산은 이러한 종류의 문제들을 예외를 이용해 처리하기 때문이죠. 만약 new 연산자가 할당할 메모리를 찾을 수 없는 경우, std::bad_alloc 예외를 던집니다. 우리는 다음과 같이 써볼 수 있겠네요:

1
2
3
4
5
6
7
8
9
10
11
12
void test()
{
    try {
        Vector v(−27);
    }
    catch (std::length_error) {
        // handle negative size
    }
    catch (std::bad_alloc) {
        // handle memory exhaustion
    }
}
cs

  여러분들은 예외를 활용해 여러분만의 클래스를 정의하고 임의의 정보를 오류가 감지된 곳으로부터 오류를 처리할 수 있는 곳으로 전달할 수 있습니다.

  가끔 어떤 함수는 예외가 발생했다면 주어진 작업을 완료할 수 없는 경우가 존재합니다. 이런 경우에 예외를 "처리"하는 것은 단순히 해당 함수의 지역적 정리를 마친 뒤 다시 예외를 던지는 경우일 것입니다.

  불변의 개념은 클래스 설계의 중심입니다. 사전조건은 함수의 설계에서 중심이 되죠. 불변은:

  • 우리가 원하는 것을 정확히 이해하도록 도와줍니다.
  • 우리를 좀 더 특정적(specific)이게 만듭니다. 곧 정확한 코드를 만들 기회를 부여합니다.(디버깅이나 테스팅에서도)

  불변의 개념은 C++의, 생성자와 소멸자를 통한 자원 관리 개념의 기반을 이루고 있습니다.


2.4.3.3 Static Assertions

  예외는 런타임에서 발생한 에러를 보고합니다. 만약 에러를 컴파일 타임에 찾을 수 있다면, 이는 보통 예방할 수 있죠. 자료형 체계와 사용자 정의 자료형의 인터페이스를 명세하는 기능들이 있는 이유입니다. 하지만 더 나아가 기타 속성들의 간단한 확인을 통해 컴파일 오류와 같이 컴파일 시기에 실패를 보고하도록 할 수 있습니다. 다음을 참고하세요.

1
static_assert(4<=sizeof(int), "integers are too small"); // check integer size
cs


  이는 만약 4<=sizeof(int)가 거짓이라면 integers are too small이라는 메시지를 출력할 것입니다. 곧 해당 시스템의 int가 4 bytes 이상은 되어야 한다는 것이죠. 이러한 예외문을 assertions라고 합니다.

  static_assert 메커니즘은 상수식으로 표현될 수 있는 항이라면 어디든지 사용될 수 있습니다.

1
2
3
4
5
6
7
8
9
constexpr double C = 299792.458// km/s
 
void f(double speed)
{
    const double local_max = 160.0/(60∗60); //160 km/h == 160.0/(60*60) km/s
    static_assert(speed<C,"can't go that fast"); // error : speed must be a constant
    static_assert(local_max<C,"can't go that fast"); // OK
    // ...
}
cs


  일반적으로, static_assert(A,S)는 만약 A가 참이 아닐 때 S 메시지와 함께 컴파일 오류를 발생시킵니다. static_assert는 제네릭 프로그래밍에서 인자로 사용되는 자료형들을 검증할 때 아주 중요하게 사용됩니다.

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기