[c] 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가 처음 ‘고토가 유해한 것으로 간주 됨’을 썼을 때 만연했던 남용은 요즘 대부분의 사람들의 마음을 가로 지르지 않습니다.

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

특정 예는 다음과 같이 단순화 할 수 있습니다 (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깊이 중첩 된 ifs로 끝나지 않고 피하고 싶다면 특히 그렇습니다 .

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

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장소를 쉽게 알아볼 수 있습니다. 패킷을 약간 다르게 설정합니다 (예 : “조건부 실행”명령이 실행하지 않기로 결정한 경우).