본문 바로가기

프로그래머/C, C++

[포프 tv 복습] 가변 인자 함수, 올바른 오류 처리 방법

가변 인자 함수, 올바른 오류 처리 방법

가변 인자 함수

<반환형> <함수명> (<자료형이 정해진 매개변수 목록>, ...);
  • 정해지지 않은 수의 매개변수(가변 인자)를 허용하는 함수
  • 반드시 최소 한 개의 정해진 자료형의 매개변수가 필요
  • 가변인자는 '...'로 표현

가변 인자 함수의 예

#include <stdarg.h>

int add_ints(const size_t count, ...)
{
    va_list ap;
    int sum;
    sisze_t i;

    sum = 0;
    va_start(ap, count);
    {
        for(i = 0; i < count; i++){
            sum += va_arg(ap, int);
        }
    }
    va_end(ap);

    return sum;
}

int main(void)
{
    int result;

    result = add_ints(1, 16);
    printf("result: %d\n", result);

    result = add_ints(4,1,2,3,4);
    printf("result: %d\n", result);

    return 0;
}

va_list

  • 가변 인자 목록
  • va_start(), va_arg(), va_end() 매크로 함수를 사용할 때 필요한 정보가 포함
  • 명시되지 않은 자료형(구현마다 다름)

va_start()

va_start(<가변 인자 목록>, <가변 인자 시작하기 직전 매개변수>);
  • 매크로 변수
  • 함수 매개변수로 들어온 가변 인자들에 접근하지 전에 반드시 호출해야 함
  • va_list에 필요한 초기화를 수행
    • 특히 가변 인자가 스택 메모리의 어디서부터 시작하는지를 찾아냄
    • 그래서 두 번째 매개변수가 필요

va_end()

va_end(<가변 인자 목록>)
  • 매크로 함수
  • 함수 매개변수로 들어온 가변 인자들에 접근이 끝난 뒤에 반드시 호출해야 함
  • 사용했던 가변 인자 목록을 정리함
    • 더 이상 가변 인자 목록을 사용할 수 없더록 가변 인자 목록의 값을

va_arg()

va_arg(<가변 인자 목록>, <얻어올 가변 인자의 자료형>);
  • 매크로 함수

  • 가변 인자 목록으로부터 다음 가변 인자를 가져옴

  • 가져올 가변 인자의 자료형은 두 번째 매개변수로 알려줌

  • 예전 표준상의 문제로 가변 인자 목록의 기본 자료형 인자들은 다음과 같이 승격됨

    • 모든 정수형은 int로
    • 모든 부동소수점은 double로
  • 따라서, 두 번째 매개변수에는 int나 double을 쓸 것

  • va_arg()는 매크로 함수

    • 함수처럼 보이지만 엄밀한 의미의 함수는 아님
    • 그 대신 전처리기가 매크로 함수의 구현 코드로 대체시켜 줌

가변 인자 함수가 인자를 읽어오는 방법

  1. va_start(ap, count)에서 가변 인자 시작 직전 매개변수(int 형)에 기초해서 가변 인자 목록의 시작 메모리 주소를 계산

    va_start(ap, count);
    => ap.data = (char*)&count + sizseof(count)
  2. va_arg(ap, int); 가 호출될 때마다 int 크기만큼 더해가며 읽을 위치를 변경하면 됨

    va_arg(ap, int);
    => val = *(int*)ap.data;
    ((int*)ap.data)++;

    따라서 va_list는 수택 메모리에서 위치를 가리키는 포인터 같은 것을 가지고 있을 수 밖에

함수에서 매개변수로 가변 인자만을 받을 수 있을까?

  • 가변 인자(...) 앞에 자료형이 특정된 매개변수가 반드시 있어야 함
  • 가변 인자 뒤에 자료형이 정해진 매개변수가 있으면 안 됨
    • 함수가 정확히 어느 오프셋에서 읽어와야 하는지 컴팡리 중에 특정 불가
  • 즉, 가변 인자 아닌 것을 우선 차례대로 읽음
  • 그 뒤, 가변 인자는 va_arg()가 시키는 대로 하나씩 주소를 늘려가며 읽는 것
void do_something(..., int);         // err
void do_something(int, ..., int);    // err
void do_somethign(int, int, ...);    // ok
  1. 가변 인자가 몇 갠지 가변 인자 함수는 모름
  2. 가변 인자의 자료형을 가변 인자는 모름

오류 처리

  • C 언어는 예외를 지원하지 않음

안 좋은 오류 처리의 예

  • 문제는 한 군데서만 찾는 게 더 효율적
  • 따라서 오류 처리를 할 때에도 원칙이 있어야 함
  • 생각 없이 무조건 작동한다고 코드 짜는 건 일단 OK
  • 그러나 그 문제를 찾는 곳은 최소한인 게 좋음

버그와 오류의 차이, 올바른 오류 처리 전략

  • assert의 문제는 실행해야만 보인다는 것
  • C89에서는 컴파일 중에 판단 가능한 것도 모두 실행해야만 보임
  • C11은 정적 어서트(static assert)로 이러한 한계를 극복

널 포인터를 허용한다면 함수나 변수에 명시

  • 함수의 매개변수가 널 포인터를 허용한다면, 매개변수 이름 끝에 'or_null'을 붙인다
  • 함수도 마찬가지
    monster_t* spawn_monster_or_null(const monster_t* special_monter_or_null)
    {
      # 코드 생략
    }

오류 코드를 반환하자!

  • 오류를 처리해주는 함수/코드에서 오류가 있음을 알려줘야 함
  • 가장 좋은 방법은 함수에서 곧바로 오류 코드 반환한느 것
    libabc_error_t try_get_student(int id, student_info_t* out_student)
    {
      size_t idx;
      // 생략
      if(idx == -1){
          return     ERROR_STUDENT_NOTFOUND;
      }
      // 생략
      return ERROR_NONE;
    }

모든 오류 코드를 하나의 enum으로 만들자

  • 구조체로 반환도 가능하나 C에서 많이 쓰는 방법은 아님
  • 오류 코드 만들 때는 해당 라이브러리에서 제공할 수 있는 모든 오류코드를 enum으로 정의하는 게 좋다
    typedef enum{
      ERROR_NONE,
      ERROR_BAD_REQUEST,
      ERROR_UNAUTHORIZED,
      ERROR_FORBIDDEN,
      ...,
    } libabc_error_t;

함수마다 오류 enum을 만드는 것은 좋지 않음

  • C#의 enum과 다르게 C의 enum은 서로 비교 및 대입이 가능

전에 본 errno도 좀 별로...

올바른 오류 처리 전략 정리

  1. 기본적으로 내가 작성하는 모든 함수에 들어오는 데이터는 유효하다 가정하고 어서트를 많이 쓸 것
  2. 그렇지 않은 함수는 매개변수나 함수 이름에서 그렇지 않다는 사실을 명백히 표시할 것
  3. 오류 상황을 처리하는 장소는 최소한으로 할 것
  4. 어떤 함수가 오류 처리를 한다는 사실을 반환형 등을 통해 확실히 보여줄 것

오류 처리 후에도 발생하는 예외 상황

운영체제의 예외 처리

  • 함수 포인터를 등록하고 OS가 보내는 예외 처리를 받아올 수는 잇음
  • 근데 받아와도 어떻게 대처해야 할지 애매한 겨웅가 있음