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


95

이 질문은 사실 얼마 전 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?



@Eli : 태그를 제거하고 if의 괄호 안에 function (cleanup_3 ();)을 배치하지 않는 이유는 무엇입니까?

@Akito : 무슨 뜻이야? 완전한 코드 샘플을 사용하여 제안을 답변으로 게시 할 수 있습니까?
Eli Bendersky 2011

@EliBendersky : 내 대답을 참조하십시오.

내가 Visual Basic (VBS 및 VB6 포함)에서 가장 싫어했던 것 중 하나는 on error goto error오류 처리 시스템 이었습니다. :)
Manu343726

답변:


64

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


1
태그를 제거하고 if에 직접 함수를 넣을 수는 없나요? 더 읽기 쉬울까요?

8
@StartupCrazy, 나는 이것이 오래되었다는 것을 알고 있지만이 사이트의 게시물의 유효성을 위해 나는 그가 할 수 없다는 것을 지적 할 것입니다. 그가 goto error3 코드에서 오류를 받으면 그는 정리 1 2와 3을 실행하고, 당신의 sugeested 솔루션에서 그는 정리 3 만 실행할 것입니다. 그는 항목을 중첩 할 수 있지만 화살표 반 패턴 일 뿐이므로 프로그래머는 피해야합니다. .
gbtimmon

18

일반적으로 고토를 피하는 것은 좋은 생각이지만, 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;
}

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


24
네, 중첩 된 솔루션은 실행 가능한 대안 중 하나입니다. 불행히도 중첩 수준이 깊어짐에 따라 실행 가능성이 떨어집니다.
Eli Bendersky

4
@eliben : 동의합니다.하지만 더 깊은 중첩은 더 많은 기능을 도입해야하거나 준비 단계에서 더 많은 작업을 수행해야하거나 그렇지 않으면 코드를 리팩터링해야한다는 표시 일 수 있습니다. 각 준비 함수가 설정을 수행하고 체인의 다음 함수를 호출하고 자체 정리를 수행해야한다고 주장 할 수 있습니다. 그 작업을 지역화합니다. 세 가지 정리 기능을 절약 할 수도 있습니다. 또한 다른 호출 시퀀스에서 설정 또는 정리 함수가 사용되는지 (사용 가능한지) 부분적으로도 다릅니다.
Jonathan Leffler

6
불행히도 이것은 루프에서 작동하지 않습니다. 루프 내부에서 오류가 발생하면 goto는 플래그 설정 및 확인 및 'break'문 (교묘하게 위장한 gotos)의 대안보다 훨씬 더 깨끗합니다.
Adam Rosenfield

1
@Smith, 방탄 조끼없이 운전하는 것과 비슷합니다.
strager

6
내가 여기서 강령하고 있다는 것을 알고 있지만,이 조언이 다소 좋지 않다고 생각합니다. 화살 방지 패턴 은 피해야합니다.
KingRadical 2013

16

나는 아무도이 대안을 제안하지 않았다는 것에 놀랐습니다. 그래서 질문이 잠시 주위에 있었지만 그것을 추가 할 것입니다.이 문제를 해결하는 한 가지 좋은 방법은 현재 상태를 추적하기 위해 변수를 사용하는 것입니다. 이것은 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);
    }
    

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

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


2
파티에 늦 으신 것 같지만 대답이 정말 마음에 들어요!

리누스은 아마 당신의 코드를 거부 할 blogs.oracle.com/oswald/entry/is_goto_the_root_of을
SX는 ageist 가십 환영

1
@ user3588161 : 그가 원한다면, 그것은 그의 특권입니다.하지만 당신이 링크 한 기사에 따르면 그게 사실인지 잘 모르겠습니다. (2) 어쨌든 필요한 것과 비교할 때 추가 "if"문이 없습니다 (모든 반환 코드를 확인한다고 가정).
psmears jul.

끔찍한 고토와 더 나쁜 화살 반 패턴 솔루션 대신에이 정도!
상자성 크로아상

8

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

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

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


8
필자는 setjmp / longjmp를 호출 된 경우에 확실히 사용했지만 goto보다 "더 나쁘다"고 생각할 것입니다 (또한 호출 될 때 다소 덜 예약 적으로 사용합니다). setjmp / longjmp를 사용하는 유일한 경우는 (1) 대상이 현재 상태의 영향을받지 않는 방식으로 모든 것을 종료하거나 (2) 대상이 제어되는 모든 것을 다시 초기화 할 때입니다. 현재 상태와 독립적 인 방식으로 setjmp / longjmp 보호 블록.
supercat 2011

4

(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 ; 
      } 
} 

1
'사례'대안을 제시해 주시겠습니까? 또한 이것이 C에서 심각한 데이터 구조 추상화에 필요한 void *와는 다른 방법이라고 주장합니다. void *를 심각하게 반대하는 사람이 없다고 생각하며 그것.
Eli Bendersky

Re : void *, 그게 바로 내 요점입니다. 어느 쪽에도 도덕적으로 잘못된 것은 없습니다. 아래 스위치 / 케이스 예. 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) {사례 2 : cleanup_2 (); 휴식; 사례 1 : cleanup_1 (); 휴식; 기본값 : break; }}
webmarc 2009

5
@webmarc, 죄송하지만 이것은 끔찍합니다! 레이블을 사용하여 goto를 완전히 시뮬레이션했습니다. 레이블에 대해 설명이없는 고유 한 값을 개발하고 스위치 / 케이스를 사용하여 goto를 구현합니다. Failure_value = 1이 "goto cleanup_something"보다 깨끗합니까?
Eli Bendersky

4
여기에 저를 설정 한 것 같습니다. 질문은 의견과 제가 선호하는 것입니다. 그러나 제가 대답을했을 때 그것은 끔찍합니다. :-( 레이블 이름에 대한 불만 사항은 나머지 예제와 마찬가지로 설명 적입니다. cleanup_1, foo, bar. 질문과 관련이 없는데 레이블 이름을 공격하는 이유는 무엇입니까?
webmarc

1
나는 "설정"할 의도가 없었고 어떤 종류의 부정적인 감정을 불러 일으켰습니다. 죄송합니다! 새로운 접근 방식이 더 이상 명확성을 추가하지 않고 '제거로 이동'만을 목표로하는 것처럼 느껴집니다. 덜 명확하지만 더 많은 코드를 사용하여 goto가 수행하는 작업을 다시 구현 한 것과 같습니다. 이 IMHO는 좋은 일이 아닙니다-고토를 없애기 위해서입니다.
Eli Bendersky

2

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

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


3
프로세서가 수행 할 수있는 모든 작업에 액세스 할 필요는 없습니다. 대부분의 경우 goto는 대안보다 더 혼란 스럽습니다.
David Thornley

@DavidThornley : 예, 프로세서가 할 수있는 모든 작업에 액세스해야합니다. 그렇지 않으면 프로세서를 낭비하게됩니다. Goto는 프로그래밍에서 최고의 지침입니다.
Ron Maimon

1

일반적으로, 나는 코드 조각이 가장 명확하게 사용하여 작성 될 수 있다는 사실을 생각 것이다 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장소를 쉽게 알아볼 수 있습니다. 패킷을 약간 다르게 설정합니다 (예 : "조건부 실행"명령이 실행하지 않기로 결정한 경우).


1

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

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


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

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


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

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


22
이것의 문제는를 완전히 추방하는 일반적인 방법은 goto깊게 중첩 된 ifs 또는 루프에서 "영리한"부울 세트를 사용하는 것입니다. 정말 도움이되지 않습니다. 아마도 당신의 도구가 더 좋아질 수도 있지만 당신 은 그렇지 않을 것이고 당신은 더 중요합니다.
Donal Fellows

1

나는 질문에 주어진 역순의 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 ;
 }

0

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

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

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

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


4
잘라낼 수는 없습니다. 정리는이 루틴에서만이 순서로 할당되는 리소스에 따라 다릅니다. 다른 곳에서는 관련이 없으므로 다른 곳에서 전화를 거는 것은 의미가 없습니다.
Eli Bendersky

0

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

중히 여기다:

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

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

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

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


0

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

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;
}

0

그러나 오래된 논의 .. "화살표 방지 패턴"을 사용하고 나중에 정적 인라인 함수에서 모든 중첩 수준을 캡슐화하는 것은 어떻습니까? 코드는 깔끔하고 최적이며 (최적화가 활성화 된 경우) goto가 사용되지 않습니다. 요컨대, 분할하고 정복하십시오. 아래 예 :

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

공간 측면에서 우리는 스택에서 변수의 세 배를 생성하고 있는데 이는 좋지 않지만 -O2로 컴파일하면 스택에서 변수를 제거하고이 간단한 예제에서 레지스터를 사용하면 사라집니다. 위의 블록에서 얻은 gcc -S -O2 test.c것은 다음과 같습니다.

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-1

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

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


2
플래그가있는 코드와 화살표 안티 패턴 (둘 다 예제에 나와 있음)은 코드를 불필요하게 복잡하게 만드는 요소입니다. "goto is evil"이외의 어떤 정당성도 사용하지 않습니다.
KingRadical 2013

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.