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.