태그 보관물: design-patterns

design-patterns

C에서 goto의 적절한 사용 사례입니까? if

“토론, 논쟁, 여론 조사, 또는 확장 된 토론을 모방하고 싶지는 않지만”C를 처음 사용하기 때문에 언어에 사용되는 일반적인 패턴에 대해 더 많은 통찰력을 얻고 싶어하기 때문에이 질문을하는 것이 주저합니다.

나는 최근에 goto명령에 대한 약간의 불쾌감을 들었지만 최근에는 그에 대한 적절한 사용 사례를 발견했습니다.

다음과 같은 코드 :

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

정리 부분이 모두 매우 유사한 경우 다음과 같이 조금 더 예쁘게 작성할 수 있습니다 (내 의견?).

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

이것이 gotoC에서 일반적이거나 수용 가능한 사용 사례 입니까? 이것을 수행하는 다른 / 더 나은 방법이 있습니까?



답변

goto문 (및 해당 라벨) 흐름 제어이다 프리미티브 (명령문의 실행 조건과 함께). 즉, 프로그램 흐름 제어 네트워크를 구성 할 수 있다는 의미입니다. 플로우 차트의 노드 간 화살표를 모델링하는 것으로 생각할 수 있습니다.

이 중 일부는 직접 선형 흐름이있는 경우 즉시 최적화 할 수 있습니다 (기본 문장 시퀀스 만 사용). 다른 패턴은 사용 가능한 구조화 된 프로그래밍 구성으로 가장 잘 대체됩니다. 그것이 보이는 경우 while루프, 용도 while루프를 확인? 구조화 된 프로그래밍 패턴은 적어도 확실히있는 가능성 의 혼란보다는 의도를 명확하게 goto진술.

그러나 C는 가능한 모든 구조화 된 프로그래밍 구조를 포함하지 않습니다. (모든 관련 항목이 아직 발견되었다는 것은 확실하지 않습니다. 지금 발견 속도가 느리지 만 모든 것이 발견되었다고 말하고 주저하고 있습니다.) 우리가 알고있는 것 중 C에는 try/ catch/ finally구조 (및 예외도). 또한 다중 레벨 break루프에서 부족 합니다. 이것들은 goto구현에 사용될 수있는 것들입니다 . 다른 스키마를 사용하여 이러한 작업을 수행 할 수도 있습니다. C에는 충분한goto프리미티브 (primitive)-그러나 종종 플래그 변수와 훨씬 더 복잡한 루프 또는 가드 조건을 생성해야합니다. 데이터 분석으로 제어 분석의 얽힘을 증가 시키면 프로그램을 전체적으로 이해하기가 더 어려워집니다. 그것은 또한 더 어려워 컴파일러가 최적화 할 수와 CPU가 빠르게 실행할 수 있도록합니다 (대부분의 흐름 제어 구조 – 그리고 확실히 goto 매우 저렴합니다 -).

따라서 goto필요한 경우 가 아니라면 사용해서는 안되며 , 존재하고 필요할 수도 있다는 것을 알고 있어야하며 , 필요한 경우 너무 나쁘지 않아야합니다. 필요한 경우의 예는 호출 된 함수가 오류 조건을 리턴 할 때 자원 할당 해제입니다. ( 그렇지만 try/ finally.) 쓸 goto수는 있지만 그렇게 하지 않으면 유지 관리 문제와 같은 자체 단점이있을 수 있습니다. 사례의 예 :

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

코드는 더 짧을 수 있지만 요점을 설명하기에 충분합니다.


답변

예.

예를 들어 리눅스 커널에서 사용됩니다. 다음 은 거의 10 년 전의 글 이 끝날 무렵에 대담한 이메일 입니다.

보낸 사람 : Robert Love
제목 : Re : 2.6.0-test *의 기회가 있습니까?
날짜 : 2003 년 1 월 12 일 17:58:06 -0500 2003 년 1 월 12 일

17시 22 분 일요일, Rob Wilkens는 다음과 같이 썼습니다.

“goto를 사용하지 마십시오”라고 말하고 대신 “cleanup_lock”함수를 사용하여 모든 return 문 앞에 추가하십시오. 부담이되어서는 안됩니다. 예, 개발자가 조금 더 열심히 일하도록 요구하고 있지만 최종 결과는 더 나은 코드입니다.

아니, 그것은 거칠고 커널을 부 풀린다 . 종료 코드를 한 번에 끝내는 것과 달리 N 오류 경로에 대한 정크를 인라인합니다.
캐시 풋 프린트가 핵심이며 방금 삭제했습니다.

읽기도 쉽지 않습니다.

마지막 주장으로, 우리는 일반적인 스택처럼 바람을 불고 풀 수 없습니다 .

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

이제 그만 해요

로버트 러브

즉, goto 사용에 익숙해지면 스파게티 코드를 작성하지 못하게하려면 많은 훈련이 필요합니다. 따라서 속도와 낮은 메모리 공간 (커널 또는 임베디드 시스템과 같은)이 필요한 것을 작성하지 않는 한 실제로 첫 번째 goto를 작성 하기 전에 생각해보십시오 .


답변

내 의견으로는, 게시 한 코드는의 올바른 사용의 예입니다. goto왜냐하면 아래로 뛰어 내리고 원시 예외 처리기처럼 사용하기 때문입니다.

그러나 오래된 goto 토론으로 인해 프로그래머는 goto약 40 년 동안 회피 해 왔 으므로 goto로 코드를 읽는 데 익숙하지 않습니다. 이것이 바로 goto를 피하는 유효한 이유입니다. 단순히 표준이 아닙니다.

C 프로그래머가 더 쉽게 읽을 수있는 코드를 다시 작성했을 것입니다.

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

이 디자인의 장점 :

  • 실제 작업을 수행하는 함수는 데이터 할당과 같이 알고리즘과 관련이없는 작업에 신경 쓸 필요가 없습니다.
  • 코드는 C 프로그래머에게 덜 외로워 보일 것입니다. 왜냐하면 그들은 프로그래머와 레이블을 두려워하기 때문입니다.
  • 알고리즘을 수행하는 함수 외부의 동일한 지점에서 오류 처리 및 할당 해제를 중앙 집중화 할 수 있습니다. 함수가 자체 결과를 처리하는 것은 의미가 없습니다.

답변

유효한 사용 사례를 설명하는 유명한 논문 은 Donald E. Knuth (Stanford University)의 GOTO Statement사용한 Structured Programming입니다 . 이 논문은 GOTO 사용이 일부 사람들에 의해 죄로 여겨 졌던 시절과 구조적 프로그래밍의 움직임이 절정에 달했을 때 나타났습니다. GoTo Thoughed Harmful을 살펴볼 수 있습니다 .


답변

Java에서는 다음과 같이합니다.

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

나는 이것을 많이 사용합니다. 내가 싫어하는 goto대부분의 다른 C 스타일 언어에서는 코드를 사용합니다. 다른 좋은 방법은 없습니다. (중첩 루프에서 점프하는 것은 비슷한 경우입니다 .Java에서는 레이블이 break있고 다른 곳에서는을 사용합니다 goto.)


답변

내가 생각 입니다 괜찮은 유스 케이스하지만, 경우에 “오류”부울 값에 불과하다, 당신이 원하는 것을 달성하기 위해 다른 방법이있다 :

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

부울 연산자의 단락 평가를 사용합니다. 이것이 “더 나은”경우, 당신의 개인적인 취향과 그 관용구에 익숙한 방법에 달려 있습니다.


답변

리눅스 스타일 가이드는 goto예제와 일치하는를 사용해야하는 특별한 이유를 제공 합니다.

https://www.kernel.org/doc/Documentation/process/coding-style.rst

gotos를 사용하는 이유는 다음과 같습니다.

  • 무조건 진술은 이해하고 따르기가 더 쉽다
  • 중첩이 줄어 듭니다
  • 수정시 개별 종료점을 업데이트하지 않아 오류 발생
  • 중복 코드를 최적화하기 위해 컴파일러 작업을 절약합니다.)

면책 조항 나는 내 일을 공유해서는 안됩니다. 여기 예제는 약간 고려되어 있으므로 곰이 나와 함께 견뎌주십시오.

메모리 관리에 좋습니다. 나는 최근에 동적으로 메모리를 할당 한 코드 (예 char *: 함수에 의해 반환 된 코드)를 연구했습니다. 경로를보고 경로의 토큰을 구문 분석하여 경로가 유효한지 확인하는 함수입니다.

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

이제 나에게 다음 코드를 추가 해야하는 경우 훨씬 훌륭하고 유지 관리가 쉽습니다 varNplus1.

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

이제 코드에는 N과 관련하여 다른 문제가 있습니다. 즉, N은 10 이상이고 함수는 450 개가 넘었으며 10 개 수준의 중첩이 있습니다.

그러나 나는 관리자에게 리팩토링을 제안했는데, 내가 한 일이 이제는 짧고 기능은 모두 리눅스 스타일입니다.

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

우리가 gotos 없이 동등한 것을 고려한다면 :

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

나에게, 첫 번째 경우, 첫 번째 함수 NULL가을 반환 하면 여기서 나가서 반환한다는 것이 분명합니다 0. 두 번째 경우에는 if가 전체 함수를 포함하는지 확인하기 위해 아래로 스크롤해야합니다. 첫 번째는 이것이 나에게 문체 적으로 (이름 ” out“) 표시하고 두 번째는 구문 적으로 표시합니다. 첫 번째는 여전히 더 분명합니다.

또한 free()함수 끝에 선언문을 사용하는 것이 좋습니다. 그것은 내 경험상 free()함수의 중간에있는 문장이 나쁜 냄새를 맡고 서브 루틴을 만들어야한다는 것을 나타 내기 때문입니다. 이 경우, 나는 var1내 기능으로 만들었고 free()서브 루틴에서 그것을 할 수 없었 습니다. 그러나 그것이 goto out_freegoto out 스타일이 그렇게 실용적인 이유 입니다.

프로그래머 goto가 사악 하다고 믿어야한다고 생각합니다 . 그런 다음 충분히 성숙 해지면 Linux 소스 코드를 탐색하고 Linux 스타일 가이드를 읽어야합니다.

나는이 스타일을 매우 일관되게 사용하고 모든 함수에는 int retval, out_freelabel 및 out 레이블 이 있다고 덧붙여 야합니다 . 스타일 일관성으로 인해 가독성이 향상되었습니다.

보너스 : 휴식과 계속

while 루프가 있다고 가정 해보십시오.

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

이 코드에는 다른 문제가 있지만, 한 가지는 continue 문입니다. 전체 내용을 다시 작성하고 싶지만 작은 방식으로 수정해야했습니다. 나를 만족시키는 방식으로 리팩토링하는 데 며칠이 걸렸지 만 실제 변화는 약 반일의 작업이었습니다. 문제는 우리가 ‘경우에도 것입니다 continue우리가 아직 확보해야’ var1하고 var2. 나는을 추가 var3해야했고, free () 문을 반영하기 위해 노력하고 싶었다.

나는 당시에 비교적 새로운 인턴이지만, 잠시 동안 리눅스 소스 코드를보고 있었기 때문에 관리자에게 goto 문을 사용할 수 있는지 물었다. 그는 그렇다고 말했습니다.

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

나는 계속되는 것이 최선이라고 생각하지만 나에게는 그들은 보이지 않는 레이블이있는 goto와 같습니다. 휴식도 마찬가지입니다. 여기서와 마찬가지로 여러 위치에서 수정 사항을 미러링하도록 강요하지 않는 한 계속 또는 중단을 선호합니다.

또한이 사용 goto next;next:레이블이 만족스럽지 않다고 덧붙여 야합니다 . free()count++진술 과 진술을 반영하는 것보다 낫습니다 .

goto의 경우는 거의 항상 잘못된 것이지만 사용하기 좋은시기를 알아야합니다.

내가 논의하지 않은 한 가지는 다른 답변에 의해 다루어 진 오류 처리입니다.

공연

strtok ()의 ​​구현을 볼 수 있습니다 http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

내가 틀렸다면 정정하십시오. 그러나 cont:레이블과 goto cont;문장이 성능을 위해 있다고 생각합니다 (확실히 코드를 읽을 수있게하지는 않습니다). 이렇게하면 읽을 수있는 코드로 대체 될 수 있습니다

while( isDelim(*s++,delim));

구분자를 건너 뛰려면 그러나 가능한 빨리하고 불필요한 함수 호출을 피하기 위해이 방법을 사용합니다.

나는 Dijkstra의 논문을 읽었고 나는 매우 난해한 것을 발견했다.

Google은 2 개 이상의 링크를 게시 할만큼 평판이 충분하지 않기 때문에 “dijkstra goto 문이 유해한 것으로 간주됩니다”.

나는 그것이 goto를 사용하지 않는 이유라고 인용 한 것을 보았고 그것을 읽는 것이 나의 goto를 사용하는 한 아무것도 변하지 않았다.

부록 :

나는이 모든 것에 대해 계속 생각하고 깨는 동안 깔끔한 규칙을 생각해 냈습니다.

  • while 루프에 continue가 있으면 while 루프의 본문은 함수 여야하며 continue는 return 문입니다.
  • while 루프에 break 문이 있으면 while 루프 자체가 함수 여야하며 break가 return 문이되어야합니다.
  • 둘 다 가지고 있다면 무언가 잘못되었을 수 있습니다.

범위 문제로 인해 항상 가능한 것은 아니지만이 작업을 수행하면 코드에 대해 추론하기가 훨씬 쉽다는 것을 알았습니다. 나는 while 루프가 끊어 지거나 계속 될 때마다 기분이 나쁘다는 것을 알았습니다.