development

C에서 오류 관리를위한 goto의 유효한 사용?

big-blog 2020. 9. 12. 11:12
반응형

C에서 오류 관리를위한 goto의 유효한 사용?


이 질문은 사실 얼마 전 programming.reddit.com에서 흥미로운 토론 의 결과입니다 . 기본적으로 다음 코드로 요약됩니다.

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

goto여기에서 사용 하는 것이 가장 좋은 방법 인 것처럼 보이며 모든 가능성 중 가장 깨끗하고 효율적인 코드를 생성하거나 적어도 나에게는 그렇게 보입니다. 코드 완성 에서 Steve McConnell 인용 :

goto는 리소스를 할당하고 해당 리소스에 대해 작업을 수행 한 다음 리소스를 할당 해제하는 루틴에서 유용합니다. goto를 사용하면 코드의 한 섹션에서 정리할 수 있습니다. goto는 오류를 감지 한 각 위치에서 리소스 할당을 잊어 버릴 가능성을 줄여줍니다.

이 접근 방식에 대한 또 다른 지원 이 섹션Linux 장치 드라이버 책에서 제공됩니다 .

어떻게 생각해? 이 경우 gotoC에서 유효한 사용 입니까? 더 복잡하거나 덜 효율적인 코드를 생성하는 다른 방법을 선호하지만 피 goto하겠습니까?


FWIF, 질문의 예에서 제공 한 오류 처리 관용구는 지금까지 답변에 제공된 대안보다 더 읽기 쉽고 이해하기 쉽습니다. goto일반적으로 나쁜 생각 이지만 간단하고 균일 한 방식으로 수행하면 오류 처리에 유용 할 수 있습니다. 이 상황에서는이지만 goto잘 정의되고 다소 구조화 된 방식으로 사용되고 있습니다.


일반적으로 고토를 피하는 것은 좋은 생각이지만, Dijkstra가 처음 'GOTO가 유해한 것으로 간주 됨'을 썼을 때 만연했던 남용은 요즘 대부분의 사람들의 마음을 가로 지르지 않습니다.

당신이 설명하는 것은 오류 처리 문제에 대한 일반화 가능한 해결책입니다. 신중하게 사용하는 한 저에게 괜찮습니다.

특정 예는 다음과 같이 단순화 할 수 있습니다 (1 단계).

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

프로세스 계속 :

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

이것은 원래 코드와 동일하다고 생각합니다. 이것은 원래 코드 자체가 매우 깨끗하고 잘 정리되어 있기 때문에 특히 깨끗해 보입니다. 종종 코드 조각은 그렇게 깔끔하지 않습니다 (하지만 그래야한다는 주장을 받아 들일 것입니다). 예를 들어, 표시된 것보다 초기화 (설정) 루틴에 전달할 상태가 더 많으므로 정리 루틴에도 전달할 상태가 더 많습니다.


나는 아무도이 대안을 제안하지 않았다는 것에 놀랐다. 그래서 질문이 잠시 주위에 있었지만 그것을 추가 할 것이다.이 문제를 해결하는 한 가지 좋은 방법은 현재 상태를 추적하기 위해 변수를 사용하는 것이다. 이것은 goto정리 코드에 도달하는 데 사용되는지 여부에 관계없이 사용할 수있는 기술입니다 . 어떤 코딩 기술 마찬가지로 장점과 단점을 가지고 있으며, 모든 상황에 적합하지 않을 것이다, 그러나 당신이 스타일을 선택하는 경우 그것의 가치는 고려 - 당신이 피하고 싶은 특히 goto중첩와 종료하지 않고 if의.

기본 아이디어는 취해야 할 모든 정리 작업에 대해 정리 작업이 필요한지 여부를 알 수있는 값의 변수가 있다는 것입니다.

goto원래 질문의 코드에 더 가깝기 때문에 버전을 먼저 보여 드리겠습니다 .

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

다른 기술에 비해 이것의 한 가지 장점은 초기화 함수의 순서가 변경 되어도 올바른 정리가 여전히 발생한다는 것입니다. 예를 들어 switch다른 답변에 설명 된 방법을 사용하면 초기화 순서가 변경되면 switch처음에 실제로 초기화되지 않은 것을 정리하려고하지 않도록 매우 신중하게 편집해야합니다.

이제 어떤 사람들은이 방법이 많은 추가 변수를 추가한다고 주장 할 수 있습니다. 실제로이 경우에는 사실입니다.하지만 실제로는 기존 변수가 필요한 상태를 이미 추적하거나 추적하도록 만들 수 있습니다. 예를 들어 prepare_stuff()실제로 malloc(), 또는에 대한 호출 인 open()경우 반환 된 포인터 또는 파일 설명자를 보유하는 변수를 사용할 수 있습니다. 예를 들면 다음과 같습니다.

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

이제 변수를 사용하여 오류 상태를 추가로 추적하면 goto초기화가 필요할수록 더 깊고 깊어지는 들여 쓰기없이 완전히 피하고 올바르게 정리할 수 있습니다.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

다시, 이것에 대한 잠재적 인 비판이 있습니다.

  • 모든 "만약"이 성능을 손상시키지 않습니까? 아니오-성공한 경우 어쨌든 모든 검사를 수행해야하기 때문입니다 (그렇지 않으면 모든 오류 사례를 검사하지 않습니다). 실패한 경우 대부분의 컴파일러는 실패한 if (oksofar)검사 의 순서를 정리 코드로 한 번만 점프하도록 최적화합니다 (GCC는 확실히 그렇습니다). 어떤 경우에도 오류 케이스는 일반적으로 성능에 덜 중요합니다.
  • 이것은 또 다른 변수를 추가하지 않습니까? 이 경우 예, 그러나 종종 return_value변수를 사용하여 oksofar여기서 수행하는 역할을 수행 할 수 있습니다 . 일관된 방식으로 오류를 반환하도록 함수를 구조화하면 if각 경우에 두 번째 오류를 피할 수도 있습니다 .

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    

    이와 같은 코딩의 장점 중 하나는 일관성이 원래 프로그래머가 반환 값을 확인하는 것을 잊은 곳이 엄지 손가락처럼 튀어 나와서 (한 클래스의) 버그를 훨씬 쉽게 찾을 수 있음을 의미한다는 것입니다.

그래서-이것은 (아직)이 문제를 해결하는 데 사용할 수있는 또 하나의 스타일입니다. 올바르게 사용하면 매우 깨끗하고 일관된 코드가 가능합니다. 다른 기술과 마찬가지로 잘못된 손에 있으면 길고 혼란스러운 코드가 생성 될 수 있습니다. :-)


goto키워드 의 문제 는 대부분 오해입니다. 평범한 악이 아닙니다. 모든 goto에서 생성하는 추가 제어 경로를 알고 있어야합니다. 코드와 그 유효성에 대해 추론하기가 어려워집니다.

FWIW, developer.apple.com 튜토리얼을 검색하면 오류 처리에 대한 goto 접근 방식을 취합니다.

우리는 gotos를 사용하지 않습니다. 반환 값이 더 중요합니다. 예외 처리는 setjmp/longjmp당신이 할 수있는 모든 것을 통해 이루어집니다 .


(void) * 포인터에 도덕적으로 잘못된 것이있는 것보다 goto 문에 대해 도덕적으로 잘못된 것은 없습니다.

도구를 사용하는 방법이 전부입니다. 제시 한 (사소한) 사례에서 case 문은 오버 헤드가 더 많지만 동일한 논리를 얻을 수 있습니다. 진짜 질문은 "내 속도 요구 사항은 무엇입니까?"입니다.

goto는 매우 빠릅니다. 특히 짧은 점프로 컴파일되도록주의하는 경우 더욱 그렇습니다. 속도가 중요한 애플리케이션에 적합합니다. 다른 응용 프로그램의 경우 유지 관리를 위해 if / else + case로 오버 헤드 적중을받는 것이 좋습니다.

기억하세요 : goto는 응용 프로그램을 죽이지 않고 개발자는 응용 프로그램을 죽입니다.

업데이트 : 다음은 사례 예입니다.

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 

GOTO가 유용합니다. 그것은 당신의 프로세서가 할 수있는 일이고 이것이 당신이 그것에 액세스해야하는 이유입니다.

때로는 함수에 약간의 무언가를 추가하고 싶을 때도 있고 단일 goto를 사용하면 쉽게 할 수 있습니다. 시간을 절약 할 수 있습니다 ..


일반적으로, 나는 코드 조각이 가장 명확하게 사용하여 작성 될 수 있다는 사실을 생각 것이다 gotoA와 증상 프로그램 흐름이 예상보다 더 복잡 즉, 일반적으로 바람직입니다. 사용을 피하기 위해 다른 프로그램 구조를 이상한 방식으로 결합 goto하면 질병보다는 증상을 치료하려고 시도 할 것입니다. 특정 예제는 goto다음 없이 구현하기가 너무 어렵지 않을 수 있습니다 .

  하다 {
    .. 조기 종료시에만 정리가 필요한 thing1 설정
    (오류) 중단되면;
    하다
    {
      .. 조기 종료시 정리가 필요한 thing2 설정
      (오류) 중단되면;
      // *****이 라인에 대한 텍스트보기
    } while (0);
    .. 정리 thing2;
  } while (0);
  .. 정리 thing1;

그러나 정리가 함수가 실패했을 때만 발생하도록되어 있었다면 첫 번째 대상 레이블 바로 앞에 goto를 넣어 케이스를 처리 할 수 ​​있습니다 return. 위의 코드는로 return표시된 줄에를 추가해야합니다 *****.

"정상적인 경우에도 정리"시나리오 에서 타깃 레이블 자체가 and / 구문 보다 "LOOK AT ME"를 훨씬 더 많이 외치므로 무엇 goto보다도 do/ while(0)구문 보다 사용 이 더 명확 하다고 생각합니다 . "오류 인 경우에만 정리"사례의 경우 문은 가독성 관점에서 볼 때 가능한 최악의 위치에 있어야합니다 (반환 문은 일반적으로 함수의 시작 부분에 있거나 그렇지 않으면 "모양"에 있어야합니다.) 끝); 가진 대상 레이블이 단지 "루프"가 끝나기 전에 일을하는 것보다 훨씬 더 쉽게 그 자격을 충족 직전.breakdowhile(0)returnreturn

BTW, 내가 가끔 goto오류 처리에 사용 하는 한 가지 시나리오 switch여러 경우에 대한 코드가 동일한 오류 코드를 공유 할 때 명령문 내에 있습니다. 내 컴파일러는 종종 여러 사례가 동일한 코드로 끝나는 것을 인식 할 수있을만큼 똑똑하지만 다음과 같이 말하는 것이 더 명확하다고 생각합니다.

 REPARSE_PACKET :
  스위치 (패킷 [0])
  {
    사례 PKT_THIS_OPERATION :
      if (문제 조건)
        PACKET_ERROR로 이동합니다.
      ... THIS_OPERATION 처리
      단절;
    사례 PKT_THAT_OPERATION :
      if (문제 조건)
        PACKET_ERROR로 이동합니다.
      ... That_OPERATION 처리
      단절;
    ...
    케이스 PKT_PROCESS_CONDITIONALLY
      if (packet_length <9)
        PACKET_ERROR로 이동합니다.
      if (패킷 [4]을 포함하는 패킷 조건)
      {
        패킷 길이-= 5;
        memmove (패킷, 패킷 +5, 패킷 _ 길이);
        REPARSE_PACKET으로 이동합니다.
      }
      그밖에
      {
        패킷 [0] = PKT_CONDITION_SKIPPED;
        패킷 [4] = 패킷 길이;
        패킷 길이 = 5;
        packet_status = READY_TO_SEND;
      }
      단절;
    ...
    기본:
    {
     PACKET_ERROR :
      packet_error_count ++;
      패킷 길이 = 4;
      패킷 [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      단절;
    }
  }   

goto문을로 대체 {handle_error(); break;}할 수 있고 do/ while(0)루프를 함께 사용 continue하여 래핑 된 조건부 실행 패킷을 처리 할 수 있지만 ,를 사용하는 것보다 더 명확하다고 생각하지 않습니다 goto. 또한 사용되는 PACKET_ERROR모든 곳 에서 코드를 복사 할 수 있고 goto PACKET_ERROR컴파일러가 복제 된 코드를 한 번 작성하고 대부분의 발생을 해당 공유 사본으로의 점프로 대체 할 수 있지만를 사용하면 goto장소를 더 쉽게 알아볼 수 있습니다. 패킷을 약간 다르게 설정합니다 (예 : "조건부 실행"명령이 실행하지 않기로 결정한 경우).


저는 개인적으로 "안전 크리티컬 코드 작성을위한 10-10 규칙의 힘"의 추종자입니다 .

내가 goto에 대해 좋은 아이디어라고 생각하는 것을 보여주는 그 텍스트의 작은 스 니펫을 포함 할 것입니다.


규칙 : 모든 코드를 매우 간단한 제어 흐름 구성으로 제한하십시오. goto 문, setjmp 또는 longjmp 구성, 직접 또는 간접 재귀를 사용하지 마십시오.

이론적 근거 : 제어 흐름이 단순 해지면 검증 기능이 강화되고 코드 명확성이 향상되는 경우가 많습니다. 재귀의 추방은 아마도 여기서 가장 놀라운 일입니다. 그러나 재귀가 없으면 비순환 함수 호출 그래프가 보장되며, 이는 코드 분석기에서 악용 될 수 있으며 제한되어야하는 모든 실행이 실제로 제한되어 있음을 증명하는 데 직접 도움이 될 수 있습니다. (이 규칙은 모든 함수가 단일 리턴 지점을 가질 것을 요구하지 않습니다. 이는 종종 제어 흐름을 단순화하기도합니다.하지만 조기 오류 리턴이 더 간단한 솔루션 인 경우가 충분합니다.)


goto 사용을 추방하는 것은 나쁘게 보이지만

규칙이 처음에 드라코 니안으로 보인다면, 문자 그대로 당신의 삶이 그 정확성에 달려있을 수있는 코드를 확인할 수 있도록하기위한 것임을 명심하십시오 : 비행하는 비행기를 제어하는 ​​데 사용되는 코드, 원자력 발전소 당신이 사는 곳에서 몇 마일이나 우주 비행사를 궤도로 운반하는 우주선. 규칙은 자동차의 안전 벨트와 같은 역할을합니다. 처음에는 약간 불편할 수 있지만 잠시 후에는 사용이 제 2의 특성이되어 사용하지 않으면 상상할 수 없게됩니다.


나는 질문에 주어진 역순의 goto 정리가 대부분의 기능에서 물건을 정리하는 가장 깨끗한 방법이라는 데 동의합니다. 하지만 때로는 함수가 어쨌든 정리되기를 원한다는 점도 지적하고 싶었습니다. 이 경우 다음 변형 if (0) {label :} 관용구를 사용하여 정리 프로세스의 올바른 지점으로 이동합니다.

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

cleanup_3정리 해야 할 것 같습니다 cleanup_2. 마찬가지로 cleanup_2정리를해야한다면 cleanup_1을 호출합니다. 당신이 할 때마다 cleanup_[n], 그것은 cleanup_[n-1]필요하므로 메서드의 책임이어야합니다 (예를 들어, cleanup_3호출하지 않고는 절대로 호출 할 수 없으며 cleanup_2누출을 일으킬 수 있음).

이 접근 방식이 주어지면 gotos 대신 정리 루틴을 호출 한 다음 반환합니다.

goto방법은없는 잘못 이나 나쁜 하지만, 그것이 반드시 "깨끗한"접근 방식 (IMHO) 아니라고 지적 단지 가치.

최적의 성능을 찾고 있다면 goto솔루션이 가장 좋다고 생각합니다 . 그러나 성능이 중요한 일부 애플리케이션 (예 : 장치 드라이버, 임베디드 장치 등)에서만 관련성이있을 것으로 기대합니다. 그렇지 않으면 코드 명확성보다 우선 순위가 낮은 마이크로 최적화입니다.


나는 주어진 코드와 관련하여 여기의 질문이 오류라고 생각합니다.

치다:

  1. do_something (), init_stuff () 및 prepare_stuff ()는이 경우 false 또는 nil을 반환하기 때문에 실패했는지 여부를 아는 것처럼 보입니다.
  2. 상태 설정에 대한 책임은 foo ()에서 직접 설정되는 상태가 없기 때문에 해당 함수의 책임 인 것으로 보입니다.

따라서 : do_something (), init_stuff () 및 prepare_stuff ()는 자체 정리를 수행 해야합니다 . do_something () 후에 정리하는 별도의 cleanup_1 () 함수가 있으면 캡슐화의 철학이 깨집니다. 나쁜 디자인입니다.

자체 정리를 수행하면 foo ()가 매우 간단 해집니다.

반면에. foo ()가 실제로 해체되어야하는 자체 상태를 생성했다면 goto가 적절할 것입니다.


내가 선호하는 것은 다음과 같습니다.

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

다음 예제에 설명 된 기술을 사용하는 것을 선호합니다 ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

출처 : http://blog.staila.com/?p=114


init 함수 Daynix CSteps의 " goto 문제 "에 대한 또 다른 솔루션으로 라이브러리를 사용 합니다. 여기여기를
참조 하십시오 .

참고 URL : https://stackoverflow.com/questions/788903/valid-use-of-goto-for-error-management-in-c

반응형