234

コード フローが次のような場合:

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

私は通常、上記の厄介なコード フローを回避するために、この回避策を見てきました。

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

この回避策/ハックを回避して、より高いレベル (業界レベル) のコードにするより良い方法は何ですか?

枠にとらわれない提案は大歓迎です!

4

27 に答える 27

312

return関数内でこれらの決定を分離し、 s の代わりにsを使用することは、許容される慣行と見なされbreakます。これらのチェックはすべて、関数と同じレベルの抽象化に対応していますが、非常に論理的なアプローチです。

例えば:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}
于 2013-08-29T09:53:17.737 に答える
260

使用することgotoが実際に正しい答えである場合があります-少なくとも、「goto質問が何であれ、答えになることは決してない」という宗教的信念で育てられていない人にとっては-そしてこれはそれらのケースの1つです.

このコードは、 をdo { ... } while(0);にドレスアップするためだけにのハックを使用しgotoていbreakます。を使用する場合はgoto、それについてオープンにしてください。コードを読みにくくしても意味がありません。

特定の状況は、非常に複雑な条件を持つコードがたくさんある場合です。

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しかし、他のソリューションよりも優れたソリューションである場合があります。

たとえば、いくつかのデータを収集していて、テストされているさまざまな条件が、ある種の「これが収集されているデータの終わりです」であると想像してください。これは、場所によって異なるある種の「継続/終了」マーカーに依存します。あなたはデータストリームにいます。

完了したら、データをファイルに保存する必要があります。

はい、合理的な解決策を提供できる他の解決策がしばしばありますが、常にそうとは限りません。

于 2013-08-29T10:05:41.363 に答える
82

bool変数で単純な継続パターンを使用できます。

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

checkNこの実行チェーンは、が返されるとすぐに停止しますfalse。オペレーターcheck...()の短絡により、それ以上の通話は実行されません。さらに、最適化コンパイラは、 toの設定が一方通行である&&ことを認識し、足りないものを挿入するほどスマートです。結果として、上記のコードのパフォーマンスは / のパフォーマンスと同じになりますが、可読性への痛烈な打撃はありません。goOnfalsegoto enddowhile(0)

于 2013-08-29T10:14:12.237 に答える
38
  1. コードを別の関数 (または複数の関数) に抽出してみてください。チェックが失敗した場合は、関数から戻ります。

  2. 周囲のコードとの結合が強すぎて、結合を減らす方法が見つからない場合は、このブロックの後のコードを見てください。おそらく、関数によって使用される一部のリソースがクリーンアップされます。RAIIオブジェクトを使用してこれらのリソースを管理してみてください。break次に、各危険をreturn(またはthrow、より適切な場合は )に置き換えて、オブジェクトのデストラクタにクリーンアップさせます。

  3. プログラム フローが (必然的に) 非常に波打っており、実際に が必要な場合はgoto、変な変装をするのではなく、それを使用してください。

  4. やみくもに を禁止するコーディング ルールがgotoあり、プログラム フローを単純化できない場合は、おそらくdoハックでそれを偽装する必要があります。

于 2013-08-29T10:01:24.813 に答える
37

TLDR : RAII、トランザクション コード (既に計算されている場合にのみ結果を設定するか、内容を返す) と例外。

長い答え:

Cでは、この種のコードのベスト プラクティスは、ローカル リソースのクリーンアップが行われ、エラー コード (存在する場合) が返されるコードにEXIT/CLEANUP/ otherラベルを追加することです。これは、コードを初期化、計算、コミット、およびリターンに自然に分割するため、ベスト プラクティスです。

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 != ...andコードは通常、いくつかの便利なマクロ ( 、など)gotoの背後に隠されています。RET(computation_result)ENSURE_SUCCESS(computation_result, return_code)

C++は、 Cにはない追加のツールを提供します。

  • クリーンアップ ブロック機能は RAII として実装できます。つまり、ブロック全体が不要になり、cleanupクライアント コードで初期の return ステートメントを追加できるようになります。

  • 続行できない場合はいつでもスローし、すべて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)、新しいコードに a を追加したり#define、goto ラッパー マクロやクリーンアップ ラベルを追加したりする必要はありません。他に何か)。

于 2013-08-29T10:52:58.320 に答える
12

コード フロー自体は、既に関数内で多くのことが起こっているコードの臭いです。それに対する直接的な解決策がない場合 (関数は一般的なチェック関数です)、RAIIを使用して、関数の最後のセクションにジャンプする代わりに戻ることができるようにすることをお勧めします。

于 2013-08-29T10:02:17.050 に答える
11

実行中にローカル変数を導入する必要がない場合は、多くの場合、これをフラット化できます。

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

// edit 
doThingsAtEndAndReportErrorStatus()
于 2013-08-29T09:55:39.683 に答える
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)

これにより、ネストされたブロックを実行し、終了/ジャンプアウトする場所を指定できます。

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

于 2013-08-30T03:38:19.693 に答える
10

例外を使用します。コードはよりきれいに見えます (また、プログラムの実行フローでエラーを処理するために例外が作成されました)。リソース (ファイル記述子、データベース接続など) のクリーンアップについては、記事「なぜ C++ は "finally" コンストラクトを提供しないのですか?」を参照してください。.

#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;
}
于 2013-08-29T19:03:52.623 に答える
8

これは、関数型プログラミングの観点からはよく知られており、よく解決されている問題です - 多分モナドです。

以下に受け取ったコメントに応えて、ここで紹介を編集しました。 Rotsor が提案することを実現できるようにするさまざまな場所でのC++モナドの実装に関する完全な詳細を見つけることができます。モナドを理解するにはしばらく時間がかかるので、代わりにここで簡単な「貧乏人」のモナドのようなメカニズムを提案します。

次のように計算ステップを設定します。

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 などを使用していくつかのバリエーションを考え出すことができます。

于 2013-08-29T13:57:18.433 に答える
5

このようなものはおそらく

#define EVER ;;

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

または例外を使用する

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

例外を使用して、データを渡すこともできます。

于 2013-08-30T08:02:40.453 に答える
3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&amp;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++;
}

どのようにそのことについて?

于 2013-09-04T16:58:07.880 に答える
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;
    }
于 2013-09-05T14:03:59.600 に答える
2

私はC++プログラマーではないので、ここではコードを書きませんが、これまでのところ、オブジェクト指向ソリューションについて言及した人は誰もいません。だからここに私の推測があります:

単一の条件を評価するメソッドを提供する汎用インターフェースを用意します。問題のメソッドを含むオブジェクトで、これらの条件の実装のリストを使用できるようになりました。リストを反復処理して各条件を評価し、失敗した場合は早期に発生する可能性があります。

良いことは、問題のメソッドを含むオブジェクトの初期化中に新しい条件を簡単に追加できるため、このような設計はオープン/クローズの原則に非常によく準拠していることです。条件の説明を返す条件評価用のメソッドを使用して、インターフェイスに 2 つ目のメソッドを追加することもできます。これは、自己文書化システムに使用できます。

ただし、欠点は、より多くのオブジェクトを使用し、リストを反復処理するため、オーバーヘッドがわずかに増えることです。

于 2013-09-05T06:38:17.943 に答える
1

goto最初に、 C++ のソリューションとして適切ではない理由を示す短い例を示します。

struct Bar {
    Bar();
};

extern bool check();

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

    Bar x;

    out:
}

これをオブジェクト ファイルにコンパイルして、何が起こるかを確認してください。次に、同等のdo++を試してくださいbreakwhile(0)

それは余談でした。要点は次のとおりです。

これらの小さなコードのチャンクは、関数全体が失敗した場合に、何らかのクリーンアップを必要とすることがよくあります。これらのクリーンアップは通常、部分的に終了した計算を「巻き戻す」ときに、チャンク自体とは逆の順序で発生したいと考えています。

これらのセマンティクスを取得する 1 つのオプションは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;
}

...等々。これは、「実行」コードの隣に「元に戻す」コードを配置するため、簡単に監査できます。簡単な監査は良いことです。また、制御フローも非常に明確になります。Cでも使えるパターンです。

関数が多くの引数を取る必要があるcalc場合がありますが、クラス/構造体が適切にまとまっていれば、通常は問題になりません。(つまり、一緒に属するものは単一のオブジェクトに存在するため、これらの関数は少数のオブジェクトへのポインターまたは参照を取得し、それでも多くの有用な作業を行うことができます。)

于 2013-09-05T02:56:00.517 に答える
1

これが私のやり方です。

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

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

  if (!check()) return;
  ...
  ...
}
于 2013-09-11T02:18:33.777 に答える
0

コードに if..else if..else ステートメントの長いブロックがある場合は、 または を使用してブロック全体を書き直すことができFunctorsますfunction pointers。それは常に正しい解決策ではないかもしれませんが、かなりの場合です。

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

于 2013-08-29T09:57:26.130 に答える
0

ifそれを 1 つのステートメントに統合します。

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

これは、goto キーワードが削除された Java などの言語で使用されるパターンです。

于 2013-08-29T14:55:54.690 に答える
0

すべてのエラーに同じエラー ハンドラを使用し、各ステップが成功を示す bool を返す場合:

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

(タイゾイドの答えに似ていますが、条件はアクションであり、&& は最初の失敗の後に追加のアクションが発生するのを防ぎます。)

于 2015-02-26T00:20:43.890 に答える
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;
}
...
...

つまり、ここで行われたことは、仕上げが関数内で分離され、物事が突然非常にシンプルでクリーンになったことです!

このソリューションは言及する価値があると思ったので、ここで提供します。

于 2013-09-28T12:18:32.123 に答える