do-while (0)을 피하는 더 좋은 방법은 무엇입니까? C ++에서 해킹?


233

코드 흐름이 다음과 같은 경우 :

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

위의 지저분한 코드 흐름을 피하기 위해 일반적 으로이 해결 방법을 보았습니다.

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

이 해결 방법 / 해킹을 피하여 더 높은 수준 (산업 수준) 코드가되는 더 좋은 방법은 무엇입니까?

즉시 사용 가능한 제안은 환영합니다!


38
RAII 및 예외 발생.
ta.speot.is는

135
나에게는 이것이 사용하기에 좋은 시간처럼 들리지만, goto누군가가 그 제안을 표시해 줄 것이라고 확신하므로 그 효과에 대한 답을 쓰지 않습니다. 긴 do ... while(0);것을 갖는 것은 잘못된 것 같습니다.
매트 피터슨

42
@dasblinkenlight : 그렇습니다. 당신이 사용하려는 경우 goto, 그것에 대해 정직하고 공개적으로 수행하여 그것을 숨길하지 않습니다 breakdo ... while
매트 피터슨을

44
@MatsPetersson : 답변을 드리겠습니다. goto재활 노력에 +1을 줄 것 입니다. :)
wilx

27
@ ta.speot.is : "RAII 및 예외 발생". 그렇게하면 예외를 제외하고 흐름 제어를 모방하게됩니다. 즉, 값 비싼 출혈 에지 하드웨어를 망치 또는 문진으로 사용하는 것과 비슷합니다. 당신은 그렇게 할 수 있지만 그것은 확실히 나에게 아주 나쁜 맛처럼 보입니다.
SigTerm

답변:


309

이러한 결정을 기능으로 분리하고 returns 대신을 사용하는 것이 허용되는 관행으로 간주됩니다 break. 이러한 모든 검사는 기능과 동일한 추상화 수준에 해당하지만 상당히 논리적입니다.

예를 들면 다음과 같습니다.

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}

22
@MatsPetersson : "함수에서 분리"는 테스트 만 수행하는 새로운 함수로 리팩토링하는 것을 의미합니다.
MSalters

35
+1. 이것은 또한 좋은 대답입니다. C ++ 11에서, 격리 된 함수 는 로컬 변수도 캡처 할 수 있으므로 람다 일 수 있습니다.
Nawaz

4
@deworde : 상황에 따라이 솔루션은 goto보다 훨씬 길고 읽기 어려워 질 수 있습니다. 불행히도 C ++은 로컬 함수 정의를 허용하지 않기 때문에 해당 함수 (귀하가 돌아올 함수)를 다른 곳으로 옮겨야합니다. 그러면 가독성이 떨어집니다. 수십 개의 매개 변수로 끝날 수 있습니다. 수십 개의 매개 변수를 갖는 것은 나쁜 일이기 때문에 새 데이터 형식을 만드는 구조체로 래핑하기로 결정합니다. 간단한 상황에서는 입력이 너무 많습니다.
SigTerm

24
@Damon return은 독자가 올바르게 작동하고 무엇을하는지 즉시 인식하기 때문에 더 깨끗합니다. 와goto 당신은 그것을 위해 무엇인지 확인하고, 반드시 실수가 만든되지 않았 음으로 주위를 둘러해야합니다. 위장 이점입니다.
R. Martinho Fernandes

11
@SigTerm : 때때로 각 함수의 크기를 결정하기 쉬운 크기로 유지하기 위해 코드를 별도의 함수로 이동해야합니다. 함수 호출에 대한 비용을 지불하지 않으려면로 표시하십시오 forceinline.
벤 Voigt

257

goto적어도 " goto질문이 무엇이든 관계없이 답이 될 수 없다 " 는 종교적 믿음에 얽매이지 않은 사람들에게는 실제로 사용하는 것이 정답 일 때가 있습니다.

이 코드의 해킹 사용 do { ... } while(0);를 드레싱의 목적에 대한 gotoA와를 break. 을 (를) 사용하려는 경우 goto공개하십시오. 코드 HARDER를 읽도록 만드는 것은 중요하지 않습니다.

특정 상황은 매우 복잡한 조건의 코드가 많은 경우입니다.

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

실제로 그러한 복잡한 논리를 갖지 않으면 코드가 정확하다는 것을 명확하게 할 수 있습니다.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

편집 : 명확히하기 위해, 나는 결코 goto일반적인 해결책으로 의 사용을 제안하지 않습니다 . 그러나 goto다른 솔루션보다 더 나은 솔루션이있는 경우 가 있습니다.

예를 들어 일부 데이터를 수집하고 있고 테스트되는 여러 조건이 일종의 "이것은 수집되는 데이터의 끝"이라고 상상해보십시오. 위치에 따라 달라지는 "계속 / 종료"마커에 따라 다릅니다. 데이터 스트림에 있습니다.

이제 완료되면 데이터를 파일로 저장해야합니다.

그렇습니다. 합리적인 솔루션을 제공 할 수있는 솔루션이 종종 있지만 항상 그런 것은 아닙니다.


76
동의하지 않는다. goto장소가있을 수 있지만 goto cleanup없습니다. RAII를 사용하여 정리를 수행합니다.
MSalters

14
@MSalters : 정리에는 RAII로 해결할 수있는 것이 포함되어 있다고 가정합니다. 어쩌면 나는 "문제 오류"또는 그와 같은 것을 말했을 것입니다.
Mats Petersson

25
따라서이 종교에 대한 종교적 신념을 가진 사람들은 아마도 goto정답이 아닙니다. 의견이 있으면 감사하겠습니다 ...
Mats Petersson 1

19
사람들이 싫어 goto당신은 / 그것을 사용하는 용도가 ... 마이크로 프로세서, 다른 한편으로는,에 내장되어있는 프로그램을 이해하고 생각하기 때문에 jumps그리고 conditional jumps문제가없는 논리 또는 뭔가 다른으로, 어떤 사람들과 함께하므로 ....
woliveirajr

17
의 적절한 사용을 위해 +1 goto. 예외가 남용 될 수 있으므로 오류가 예상되는 경우 (예외 가 아님) RAII가 올바른 솔루션 이 아닙니다 .
Joe

82

bool변수 와 함께 간단한 연속 패턴을 사용할 수 있습니다 .

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

이 실행 체인은을 checkN반환 하자마자 중지됩니다 false. 작업자의 check...()단락으로 인해 더 이상 전화를 걸지 않습니다 &&. 또한, 최적화 컴파일러는 해당 설정을 인식하는 스마트 충분히 goOnfalse일방 통행로이며, 누락을 삽입 goto end당신을 위해. 그 결과, 코드의 성능은 위의 것과 동일한 것 do/ while(0)만 가독성에 고통스러운 타격하지 않고.


30
내부 if조건은 매우 의심스러워 보입니다 .
Mikhail

90
@Mikhail 훈련되지 않은 눈에만.
dasblinkenlight

20
나는 전에이 기술을 사용했는데 항상 가지 내가 컴파일러가 각각 확인되지하는 코드를 생성 할 것입니다 생각 저를 도청 if상관없이 goOn(탈옥 / 점프 반대) 초기 하나가 실패하더라도이었다 ... 그러나 나는 방금 테스트를했고 VS2012는 어쨌든 첫 번째 거짓 후에 모든 것을 단락시킬만큼 똑똑했습니다. 나는 이것을 더 자주 사용할 것이다. 참고 : 당신이 사용하는 경우 goOn &= checkN()다음 checkN()항상 경우에도 실행 goOn했다 false의 시작 부분 if(즉, 그렇게하지 않습니다).
마크

11
@ Nawaz : 코드베이스 전체에서 임의의 변경을 수행하는 훈련되지 않은 마음이 있다면 ifs 내부 의 할당보다 훨씬 큰 문제가 있습니다 .
Idelic

6
@sisharp Elegance는 보는 사람의 눈에 너무나! 나는 어떻게 루핑 구조의 오용이 어떻게 든 "우아한"것으로 인식 될 수 있는지 이해하지 못하지만 아마도 그저 나일 뿐이다.
dasblinkenlight

38
  1. 코드를 별도의 함수 (또는 둘 이상)로 추출하십시오. 점검에 실패하면 기능에서 복귀하십시오.

  2. 주변 코드와 너무 밀접하게 연결되어 있고 커플 링을 줄일 수있는 방법을 찾지 못하면이 블록 뒤의 코드를 확인하십시오. 아마도 함수에서 사용하는 일부 리소스를 정리합니다. RAII 오브젝트를 사용하여 이러한 자원을 관리하십시오 . 그런 다음 각 피지 breakreturn(또는 throw더 적절한 경우) 로 바꾸고 객체의 소멸자를 정리하십시오.

  3. 프로그램 흐름이 (필요하게) 너무 구불 구불하게되어 정말로 필요하다면 goto이상한 변장을주는 대신 사용하십시오.

  4. 맹목적으로 금지하는 코딩 규칙이 goto있고 실제로 프로그램 흐름을 단순화 할 수 없다면 do해킹 으로 위장해야 할 것입니다 .


4
RAII는 유용하지만 은총 알이 아님을 겸손히 제출합니다. 다른 용도가없는 RTO 변환 클래스를 작성하려고 할 때 이미 언급 한 "세계 최첨단"관용구를 사용하는 것이 좋습니다.
idoby

1
@busy_wait : 사실, RAII는 모든 것을 해결할 수는 없습니다. 그렇기 때문에 제 대답이 두 번째 요점으로 끝나지 않지만 goto실제로 더 나은 옵션인지 제안하려고 계속합니다 .
Mike Seymour

3
동의하지만 go-RAII 변환 클래스를 작성하는 것은 좋지 않은 생각이라고 명시 적으로 언급해야한다고 생각합니다.
idoby

@idoby 하나의 RAII 템플릿 클래스를 작성 하고 람다에서 필요한 단일 사용 정리로 인스턴스화
Caleth

@Caleth는 ctors / dtors를 재창조하는 것처럼 들립니다.
idoby

37

TLDR : RAII , 트랜잭션 코드 (결과가 이미 설정되어있을 때만 결과를 설정하거나 반환) 및 예외.

긴 대답 :

에서 C 코드의 이런 종류의 가장 좋은 방법은 EXIT / CLEANUP / 추가하는 것입니다 다른 지역 자원의 정리가 발생하고 오류 코드 (있는 경우)이 반환되는 경우, 코드에서 레이블을. 이것은 코드를 자연스럽게 초기화, 계산, 커밋 및 리턴으로 나누기 때문에 모범 사례입니다.

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

C, 대부분의 코드베이스에서, if(error_ok != ...그리고 goto코드는 일반적으로 몇 가지 편리한 매크로 (뒤에 숨겨진 RET(computation_result), ENSURE_SUCCESS(computation_result, return_code)등).

C ++ 을 통해 제공 추가 도구를 C :

  • 정리 블록 기능은 RAII로 구현 될 수 있습니다. 즉, 더 이상 전체 cleanup블록이 필요하지 않으며 클라이언트 코드가 초기 리턴 명령문을 추가 할 수 있습니다.

  • 계속할 수 없을 때마다 던져 모든 if(error_ok != ...직통 전화로 변환 합니다.

동등한 C ++ 코드 :

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

이것은 다음과 같은 이유로 모범 사례입니다.

  • 명시 적입니다 (즉, 오류 처리가 명시 적이 지 않지만 알고리즘의 주요 흐름은 다음과 같습니다)

  • 클라이언트 코드를 작성하는 것은 간단합니다

  • 최소한입니다

  • 간단하다

  • 반복적 인 코드 구성이 없습니다

  • 매크로를 사용하지 않습니다

  • 이상한 do { ... } while(0)구문을 사용하지 않습니다

  • 최소한의 노력으로 재사용 할 수 있습니다 (즉, 호출을 computation2();다른 함수 에 복사하려는 경우 do { ... } while(0)새 코드를 추가 하거나 #definegoto wrapper 매크로 및 정리 레이블을 추가 할 필요가 없습니다. 다른 것).


+1. 이것이 RAII 등을 사용하여 사용하려고하는 것입니다. shared_ptr맞춤 삭제 도구를 사용하면 많은 작업을 수행 할 수 있습니다. C ++ 11의 람다를 사용하면 훨씬 쉽습니다.
Macke

: 사실,하지만 당신은 포인터하지 않은 것들에 대한 shared_ptr의를 사용하게하는 경우, 그것을 형식 정의 - 보내고 적어도 고려하지 않습니다 namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };(이 경우에 make_handle더 이상 제안이 포인터이며, 건설의 올바른 Deleter가 유형을 설정) 유형의 이름을 .
utnapistim

21

완전성을 위해 답변을 추가하고 있습니다. 다른 많은 답변은 큰 조건 블록이 별도의 기능으로 나눌 수 있다고 지적했습니다. 그러나 여러 번 지적 했듯이이 접근법은 조건부 코드를 원래 컨텍스트와 분리한다는 것입니다. 이것이 C ++ 11의 언어에 람다가 추가 된 이유 중 하나입니다. 람다 사용은 다른 사람들에 의해 제안되었지만 명확한 샘플은 제공되지 않았습니다. 나는이 답변에 하나를 넣었습니다. 나를 때리는 것은 do { } while(0)여러면 에서 접근 방식 과 매우 유사하다는 느낌입니다. 아마도 여전히 goto위장에 있음을 의미합니다 ....

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations

7
나 에게이 해킹은 해킹하는 동안보다 나빠 보입니다.
Michael

18

확실히 대답하지만 답변 (완전성을 위해서)

대신에 :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

당신은 쓸 수 있습니다 :

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

이것은 아직도이다 고토 변장하지만, 적어도 그것은 루프는 더 이상 아니다. 즉, 일부를 계속 점검하지 않아도 매우 신중하게 확인할 필요가 없습니다. 블록의 숨겨진 곳은.

구조는 컴파일러가 최적화하기를 희망 할 정도로 간단합니다.

@jamesdlin이 제안한 것처럼 매크로 뒤에 숨길 수도 있습니다.

#define BLOC switch(0) case 0:

그리고 그것을 사용하십시오

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

이는 C 언어 구문이 대괄호 블록이 아니라 스위치 뒤의 명령문을 예상하고 해당 명령문 앞에 케이스 레이블을 넣을 수 있기 때문에 가능합니다. 지금까지 나는 그것을 허용하는 요점을 보지 못했지만이 특별한 경우 스위치를 멋진 매크로 뒤에 숨기는 것이 편리합니다.


2
오 영리하다. 매크로 뒤에 숨기고처럼 define BLOCK switch (0) case 0:사용할 수도 BLOCK { ... break; }있습니다.
jamesdlin

@ jamesdlin : 스위치의 열기 브래킷 앞에 케이스를 넣는 것이 유용 할 수 있기 전에 그것은 나에게 결코 발생하지 않았습니다. 그러나 실제로 C에 의해 허용 되며이 경우 멋진 매크로를 작성하는 것이 편리합니다.
kriss September

15

나는 매트 답변과 비슷한 접근 방식에서 불필요한 것을 뺀 것을 추천합니다 goto. 조건부 논리 만 함수에 넣으십시오. 항상 실행되는 모든 코드는 호출자에서 함수가 호출되기 전후에 수행되어야합니다.

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}

3
의 중간에 다른 리소스를 가져와야하는 경우 func패턴별로 다른 함수를 분해해야합니다. 이러한 모든 격리 된 함수에 동일한 데이터가 필요한 경우 동일한 스택 인수를 반복해서 복사하거나 힙에 인수를 할당하고 언어 기능의 가장 기본적인 기능을 활용하지 않고 포인터를 전달하도록 결정합니다. 함수 인수). 요컨대,이 솔루션은 정리 해야하는 새로운 자원을 얻은 후 모든 조건을 확인하는 최악의 경우에 확장 될 것이라고 생각하지 않습니다. 그러나 Nawaz는 람다에 대해 언급합니다.
tne

2
OP가 코드 블록 중간에 리소스를 얻는 것에 대해 아무 말도하지 않지만 나는 그것을 실현 가능한 요구 사항으로 받아들입니다. 이 경우 스택의 리소스를 어디에서나 선언 func()하고 소멸자가 리소스 해제를 처리하도록 허용 하면 무엇이 문제 입니까? 외부 func()자원이 동일한 자원에 액세스해야하는 경우 func()적절한 자원 관리자 가 호출하기 전에 힙에 선언해야합니다 .
Dan Bechard

12

코드 흐름 자체는 이미 많은 기능에서 발생하는 코드 냄새입니다. 그것에 대한 직접적인 해결책이 없다면 (함수는 일반적인 검사 기능입니다) RAII 를 사용 하면 함수의 끝 부분으로 건너 뛰는 대신 돌아갈 수 있습니다.


11

실행 중에 로컬 변수를 도입 할 필요가 없다면 종종 이것을 평평하게 할 수 있습니다 :

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()

2
그러나 각 조건에는 이전 조건이 포함되어야하는데 이는 추악 할뿐만 아니라 잠재적으로 성능이 나빠질 수 있습니다. 우리가 "좋지 않다"는 것을 알게 되 자마자 즉시 점검과 정리를 뛰어 넘는 것이 좋습니다.
tne

사실이지만,이 방법은 마지막에해야 할 일이있어 일찍 돌아가고 싶지 않다면 이점이 있습니다 (편집 참조). 조건에서 부울을 사용하는 Denise의 접근 방식과 결합하면 매우 엄격한 루프가 아닌 한 성능 적중은 무시할 수 있습니다.
the_mandrill

10

dasblinkenlight의 답변과 비슷하지만 if코드 검토자가 "고정"할 수있는 내부 할당을 피하십시오 .

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

다음 단계 전에 단계 결과를 확인해야 할 때이 패턴을 사용하는데, 이는 모든 검사가 큰 if( check1() && check2()...유형의 패턴 으로 미리 수행 될 수있는 상황과 다릅니다 .


check1 ()은 실제로 단계의 결과 코드를 리턴하는 PerformStep1 () 일 수 있습니다. 이렇게하면 프로세스 흐름 기능의 복잡성이 줄어 듭니다.
Denise Skidmore

10

예외를 사용하십시오. 코드가 훨씬 깔끔해 보일 것입니다 (프로그램의 실행 흐름에서 오류를 처리하기 위해 예외가 정확하게 생성되었습니다). 리소스 (파일 설명자, 데이터베이스 연결 등)를 정리하려면 C ++에서 "최종"구문을 제공하지 않는 이유 문서를 읽으십시오 . .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}

10
정말? 첫째, 솔직히 가독성 향상이 보이지 않습니다. 프로그램 흐름을 제어하기 위해 예외를 사용하지 마십시오. 그것은 그들의 올바른 목적이 아닙니다. 또한 예외로 인해 성능이 크게 저하 될 수 있습니다. 예외에 대한 올바른 사용 사례는 다른 프로세스에 의해 이미 독점적으로 잠겨있는 파일을 열려고하거나 네트워크 연결이 실패하거나 데이터베이스 호출이 실패하는 등 어떤 조건도 존재하지 않는 경우입니다. 또는 호출자가 잘못된 매개 변수를 프로 시저에 전달합니다. 그런 종류의 것. 그러나 프로그램 흐름을 제어하기 위해 예외를 사용하지 마십시오.
Craig

3
나는 그것에 대해 코딱지가 아니라 언급 한 Stroustrup 기사로 이동하여 "무엇을 예외로 사용해서는 안됩니까?"를 찾으십시오. 부분. 무엇보다도, 그는 말한다 : "특히, 던져 단순히 (반환 유사) 함수에서 값을 반환하는 또 다른 방법이 아니다 이렇게하면 느려질 수 및 오류에만 사용 예외를 seing에 사용되는 대부분의 C ++ 프로그래머를 혼동한다. 마찬가지로, throw는 루프를 벗어나는 좋은 방법이 아닙니다. "
Craig

3
@Craig 당신이 지적한 모든 것이 맞지만, 당신은 샘플 프로그램이 check()조건이 실패한 후에도 계속 될 수 있다고 가정하고 있습니다 . 프로그램을 계속할 수 없다고 가정하면 예외를 사용하는 것이 좋습니다.
Cartucho

2
사실 그것이 문맥이라면 충분합니다. 그러나, 가정에 관한 재미있는 것은 ... ;-)
Craig

10

나를 위해 do{...}while(0)괜찮습니다. 당신이보고 싶지 않으면do{...}while(0) 대체 키워드를 정의 할 수 있습니다.

예:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST

컴파일러가 불필요한 while(0)조건을 제거 할 것이라고 생각합니다 .do{...}while(0) 바이너리 버전에서 하고 나누기를 무조건 점프로 변환 할 . 어셈블리 언어 버전인지 확인할 수 있습니다.

를 사용하면 goto코드가 더 깨끗 해지고 조건 논리 점프 로직이 간단합니다. 다음을 수행 할 수 있습니다.

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.

레이블은 닫은 후에 배치됩니다 }. 이것은 goto레이블이 보이지 않기 때문에 실수로 코드를 넣는 문제를 피하는 것 입니다. 그것은 지금 같다do{...}while(0)조건 코드가없는 .

이 코드를보다 명확하고 이해하기 쉽게하려면 다음과 같이하십시오.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

이를 통해 중첩 블록을 수행하고 종료 / 점프 할 위치를 지정할 수 있습니다.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

스파게티 코드는 goto의 잘못이 아니라 프로그래머의 잘못입니다. 를 사용하지 않고도 스파게티 코드를 계속 생성 할 수 있습니다 goto.


10
goto처리기를 사용하여 언어 구문을 백만 번 이상 확장하는 것을 선택 했습니다.
Christian

2
"이 코드를보다 명확하고 이해하기 쉽게하려면 [LOADS_OF_WEIRD_MACROS]를 사용할 수 있습니다." : 계산하지 않습니다.
underscore_d

8

이것은 기능적 프로그래밍 관점에서 볼 수있는 잘 알려진 문제입니다. 아마도 모나드 일 것입니다.

아래에서받은 의견에 대한 응답으로 여기에서 소개를 편집했습니다 .Rotsor가 제안한 것을 달성 할 수 있는 다양한 장소 에서 C ++ 모나드 구현에 대한 자세한 내용을 찾을 수 있습니다 . 모나드를 잡는 데 시간이 걸리므로 대신 boost :: optional에 대해 알아야 할 빠른 "가난한"모나드 유사 메커니즘을 제안하겠습니다.

다음과 같이 계산 단계를 설정하십시오.

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

boost::none주어진 계산 단계 가 비어 있다면 각 계산 단계는 분명히 return과 같은 것을 할 수 있습니다 . 예를 들어 :

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

그런 다음 함께 연결하십시오.

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

이것에 대한 좋은 점은 각 계산 단계마다 명확하게 정의 된 단위 테스트를 작성할 수 있다는 것입니다. 또한 호출은 일반 영어처럼 읽습니다 (보통 기능적 스타일의 경우와 동일).

불변성에 신경 쓰지 않고 shared_ptr 등을 사용하여 변형을 일으킬 수있을 때마다 동일한 객체를 반환하는 것이 더 편리합니다.


3
이 코드는 각각의 개별 함수가 이전 함수의 실패를 처리하도록 강제하는 바람직하지 않은 속성을 가지고 있으므로 Monad 관용구를 제대로 활용하지 못합니다 (이 경우 모나드 효과는 암시 적으로 처리되어야합니다). 그러기 위해서는 optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);함수 애플리케이션 대신 모나 딕 컴포지션 연산 ( '바인드')을 사용해야합니다.
Rotsor

감사. 당신은 맞습니다-그것이 올바르게하는 방법입니다. 나는 그 대답을 설명하기 위해 너무 많이 쓰고 싶지 않았다.
베네딕트

7

if 문을 여분의 함수로 옮기면 숫자 또는 열거 형 결과가 나옵니다.

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}

이것은 가능하면 좋지만 여기서 묻는 질문보다 훨씬 덜 일반적입니다. 모든 조건은 마지막 분기 제거 테스트 후에 실행 된 코드에 따라 달라질 수 있습니다.
kriss

여기서 문제는 분리 된 원인과 결과입니다. 즉, 동일한 조건 번호를 나타내는 별도의 코드가 있으며 이는 추가 버그의 원인이 될 수 있습니다.
Riga

@kriss : 음, ConditionCode () 함수는 이것을 처리하도록 조정할 수 있습니다. 이 기능의 핵심은 최종 조건이 계산 되 자마자 깨끗한 종료에 return <result>를 사용할 수 있다는 것입니다. 이것이 바로 구조적 명확성을 제공하는 것입니다.
karx11erx

@Riga : 이모는 완전히 학문적 반대입니다. C ++은 모든 새 버전에서 더 복잡하고 암호가 없으며 읽을 수 없게됩니다. 대상 함수를보다 읽기 쉽게 만들기 위해 복잡한 구조를 잘 구조화 된 방식으로 평가하는 작은 도우미 함수의 문제를 볼 수 없습니다.
karx11erx

1
@ karx11erx 내 이의 제기는 실용적이고 내 경험에 근거합니다. 이 패턴은 C ++ 11 또는 다른 언어와 관련이 없습니다. 언어 구조에 어려움이있는 경우 언어 문제가 아닌 훌륭한 아키텍처를 작성할 수 있습니다.
리가

5

아마도 이런 것

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

또는 예외를 사용하십시오

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

예외를 사용하면 데이터를 전달할 수도 있습니다.


10
정의와 같은 영리한 일을하지 마십시오. 보통 동료 개발자가 코드를 읽기 어렵게 만듭니다. 헤더 파일에서 Case를 break; case로 정의하고 cpp 파일의 스위치에서이를 사용하여 누군가가 스위치가 Case 문 사이에서 전환되는 이유를 궁금해합니다. Grrr ...
Michael

5
매크로의 이름을 지정할 때 매크로처럼 보이게해야합니다 (즉, 모두 대문자). 그렇지 않으면 변수 / 함수 / 유형 / 등의 이름을 짓는 사람. 라는 ever매우 불행이 될 것입니다 ...
jamesdlin

5

나는 특별히 사용하지 break않거나return 이러한 케이스. 일반적으로 이러한 상황에 직면 할 때 일반적으로 비교적 긴 방법입니다.

종료 점이 여러 개인 경우 특정 논리가 실행되는 원인을 알고 자 할 때 어려움을 겪을 수 있습니다. 일반적으로 해당 논리를 포함하는 블록을 계속 올라가고 해당 폐쇄 블록의 기준에 따라 상태:

예를 들어

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

둘러싸는 블록을 보면 사실 myLogic()일 때만 발생 한다는 것을 쉽게 알 수 conditionA and conditionB and conditionC있습니다.

조기 반품이있을 때 눈에 띄지 않게됩니다.

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

더 이상을 (를) 탐색 myLogic()하고 조건을 파악하기 위해 둘러싸는 블록을 살펴볼 수 없습니다 .

내가 사용한 다른 해결 방법이 있습니다. 다음 중 하나입니다.

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(물론 동일한 변수를 사용하여 모두를 대체하는 것이 좋습니다. isA isB isC 좋습니다.)

이러한 접근 방식은 적어도 독자에게 코드를 제공하며,이 myLogic()경우 실행됩니다 isB && conditionC. 독자에게는 isB가 참이되는 원인을 추가로 찾아보아야한다는 힌트가 제공됩니다.


3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

어떻게에 대한?


if는 더 이상 사용되지 않으며, return condition;그렇지 않으면 이것이 잘 유지 관리 가능하다고 생각합니다.
SpaceTrucker

2

실패 위치에 따라 다른 정리 단계가 필요한 경우 유용한 또 다른 패턴 :

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }

2

저는 C ++ 프로그래머가 아니므로 여기에 코드를 쓰지 않지만 객체 지향 솔루션을 언급 한 사람은 없습니다. 여기에 내 추측이 있습니다.

단일 조건을 평가하는 방법을 제공하는 일반 인터페이스가 있습니다. 이제 해당 메소드가 포함 된 객체에서 해당 조건의 구현 목록을 사용할 수 있습니다. 목록을 반복하고 각 조건을 평가하여 실패하면 조기에 깨질 수 있습니다.

좋은 점은 그러한 디자인이 개방 / 폐쇄 원칙에 매우 잘 부합한다는 것입니다 . 문제의 메소드를 포함하는 객체를 초기화하는 동안 새로운 조건을 쉽게 추가 할 수 있기 때문입니다. 조건에 대한 설명을 반환하는 조건 평가 방법을 사용하여 인터페이스에 두 번째 방법을 추가 할 수도 있습니다. 자체 문서화 시스템에 사용할 수 있습니다.

그러나 단점은 더 많은 객체를 사용하고 목록을 반복하기 때문에 약간 더 많은 오버 헤드가 있다는 것입니다.


다른 언어로 예제를 추가 할 수 있습니까? 나는이 질문이 C ++에 대해 특별히 요청되었지만 많은 언어에 적용된다고 생각합니다.
Denise Skidmore

1

이것이 내가하는 방법입니다.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}

1

먼저 gotoC ++에 적합한 솔루션이 아닌 이유를 보여주는 간단한 예입니다 .

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

이것을 객체 파일로 컴파일하고 무슨 일이 일어나는지보십시오. 그런 다음 동등한 do+ break+while(0) .

그것은 제쳐두고 있었다. 요점은 다음과 같습니다.

코드의 그 작은 덩어리가 종종 필요 일부 정리 종류의 전체 기능이 실패합니다. 이러한 정리는 일반적 으로 부분적으로 완료된 계산을 "풀기"때문에 청크 자체와 반대 순서 로 발생하려고합니다 .

이러한 의미를 얻는 한 가지 옵션은 RAII입니다. . @utnapistim의 답변을 참조하십시오. C ++은 자동 소멸자가 생성자와 반대 순서로 실행되도록 보장합니다.

그러나 많은 RAII 클래스가 필요합니다. 때로는 더 간단한 옵션이 스택을 사용하는 것입니다.

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...등등. "do"코드 옆에 "undo"코드를 넣기 때문에 감사하기 쉽습니다. 쉬운 감사가 좋습니다. 또한 제어 흐름을 매우 명확하게 만듭니다. C에도 유용한 패턴입니다.

calc함수가 많은 인수를 필요로 할 수 있지만 클래스 / 구조체가 잘 응집되어 있으면 일반적으로 문제가되지 않습니다. 즉, 함께 속하는 것은 단일 객체에 존재하므로 이러한 함수는 적은 수의 객체에 대한 포인터 또는 참조를 가져와 여전히 많은 유용한 작업을 수행 할 수 있습니다.


정리 경로를 감사하는 것은 쉽지만 황금 경로를 추적하는 것은 쉽지 않습니다. 그러나 전반적으로 이와 같은 것이 일관된 정리 패턴을 장려한다고 생각합니다.
Denise Skidmore

0

코드에 if..else if..else 문의 긴 블록이 있으면 Functors또는 의 도움으로 전체 블록을 다시 작성해 볼 수 있습니다 function pointers. 항상 올바른 해결책은 아니지만 상당히 자주 있습니다.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html


원칙적으로 이것은 가능하지만 나쁘지는 않지만 명시 적 함수 객체 또는 포인터를 사용하면 코드 흐름이 너무 강력하게 중단됩니다. 람다는 여기서 람다 또는 일반적인 명명 된 함수를 사용하는 것이 실용적이고 효율적이며 잘 읽 힙니다.
leftaroundabout

0

여기에 제시된 다양한 답변이 놀랍습니다. 그러나 마지막으로 변경 해야하는 코드에서 (예 : 이것을 제거하십시오)do-while(0) 해킹 또는 다른 것을 )에서 여기 언급 된 답변과 다른 것을 수행했으며 아무도 이것을 생각하지 않은 이유가 혼란 스럽습니다. 내가 한 일은 다음과 같습니다.

초기 코드 :

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

지금:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

따라서 여기서 수행 된 작업은 마무리 작업이 기능으로 분리되어 일이 갑자기 너무 간단하고 깨끗해 졌다는 것입니다.

나는이 솔루션이 언급 할 가치가 있다고 생각했기 때문에 여기에 제공했다.


0

if문장 으로 통합하십시오 .

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

이것은 goto 키워드가 제거 된 Java와 같은 언어에서 사용되는 패턴입니다.


2
조건 테스트 사이에 아무것도 할 필요가없는 경우에만 작동합니다. (물론, 조건 테스트에 의해 작성된 일부 함수 호출 내에서 작업 수행을 숨길 수 있다고 생각하지만 너무 많이 수행하면 약간 혼란 스러울 수 있습니다)
Jeremy Friesner

@JeremyFriesner 실제로, 당신은 실제로 사이에 물건을 항상로 평가하는 별도의 부울 함수로 할 수 true있습니다. 단락 평가를 통해 모든 전제 조건 테스트가 통과하지 않은 사이를 실행하지 않도록 보장합니다.
AJMansfield

@ AJMansfield 예, 그것이 두 번째 문장에서 언급 한 것입니다 ...하지만 그렇게하면 코드 품질이 향상 될지 확실하지 않습니다.
Jeremy Friesner

@JeremyFriesner로 조건을 작성하는 것을 막을 수있는 것은 없습니다 (/*do other stuff*/, /*next condition*/). 사람들이 그것을 좋아할 것이라고 기대하지 마십시오. 그러나 정직하게, 이것은 단지는 그 단계적으로 자바의 실수 보여 간다 goto... 문
cmaster - 분석 재개 모니카

@JeremyFriesner 나는 이것이 부울이라고 가정했습니다. 각 조건 내에서 함수를 실행해야한다면 처리하는 것이 더 좋습니다.
Tyzoid

0

모든 오류에 대해 동일한 오류 처리기를 사용하고 각 단계에서 성공을 나타내는 부울을 반환합니다.

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(tyzoid의 답변과 유사하지만 조건은 동작이며 &&는 첫 번째 실패 후 추가 동작이 발생하지 않도록합니다.)


0

왜 신고 방법에 응답하지 않았습니까?

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.