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 2013

135
私には、これは使用するのに良いタイミングのようgotoに思えますが、誰かがそれを提案したことで私に値下げされたと確信しているので、その効果に対する答えは書いていません。LONGを持つdo ... while(0);ことは間違っているようです。
Mats Petersson 2013

42
@dasblinkenlight:はい、確かに。を使用する場合はgoto、それについて正直に言い、戸外でそれをbreak行い、and を使用してそれを隠さないでくださいdo ... while
Mats Petersson

44
@MatsPetersson:それを答えにしてください、私はあなたのgotoリハビリ活動のためにあなたに+1を与えます。:)
wilx 2013

27
@ ta.speot.is:「RAIIと例外のスロー」。そうすることで、例外を伴うフロー制御をエミュレートすることになります。つまり、ハンマーやペーパーウェイトとして高価で最先端のハードウェアを使用するようなものです。あなたはそれを行うことができますが、それは間違いなく私にとって非常に悪い味のように見えます。
SigTerm 2013

回答:


309

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

例えば:

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

22
@MatsPetersson:「関数に分離」とは、テストのみを行う新しい関数にリファクタリングすることを意味します。
MSalters 2013

35
+1。これも良い答えです。C ++ 11では、分離された関数はラムダにすることができます。これは、ローカル変数もキャプチャできるため、物事を簡単にするためです。
Nawaz 2013

4
@deworde:状況によっては、このソリューションはgotoよりはるかに長く、読みにくくなる場合があります。残念ながら、C ++ではローカル関数の定義が許可されていないため、その関数(元の関数)を別の場所に移動する必要があり、読みやすさが低下します。何十ものパラメーターを持つことになるかもしれません。それから、何十ものパラメーターを持つことは悪いことなので、それらを構造体にラップして、新しいデータ型を作成することにします。単純な状況では入力が多すぎます。
SigTerm 2013

24
@Damonはどちらかと言えば、returnそれが正しく機能していることを読者がすぐに認識できるため、よりクリーンです。ではgoto、あなたはそれが何のためにあるのか確認するために、そして間違いがなされなかったことを確認するために周りを見ています。偽装メリットです。
R.マルティーニョフェルナンデス

11
@SigTerm:時々、コードを個別の関数に移動して、各関数のサイズを理由のわかりやすいサイズに抑える必要があります。関数呼び出しの料金を支払いたくない場合は、それをマークしforceinlineます。
Ben Voigt 2013

257

を使用すること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、他のソリューションよりも優れたソリューションであります。

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

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

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


76
同意しない。goto場所があるかもしれgoto cleanupませんが、ありません。クリーンアップはRAIIで行われます。
MSalters 2013

14
@MSalters:これは、クリーンアップにRAIIで解決できる何かが含まれていることを前提としています。多分私は代わりに「問題エラー」またはそのようないくつかを言ったはずです。
Mats Petersson 2013

25
だから、おそらくこれについての反対投票を続けてください、おそらく宗教的信念のものからの投票gotoは決して正しい答えではありません。コメントがありましたら
幸い

19
gotoあなたはそれを使うことを考えなければならない/それを使うプログラムを理解する必要があるので人々は嫌いです...その一方で、マイクロプロセッサは組み込まれjumpsていconditional jumpsます...そして問題はロジックや他のものではなく一部の人にあります。
woliveirajr 2013

17
を適切に使用するための+1 goto。エラーが予想される場合(例外ではない)、RAIIは正しいソリューションではありません。これは例外の乱用となるためです。
ジョー

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条件内の割り当ては非常に疑わしく見えます。
ミハイル

90
@Mikhail訓練を受けていない目だけに。
dasblinkenlight 2013

20
私は以前このテクニックを使ったifことgoOnがありますが、初期のコードが失敗した場合でも(ジャンプ/ブレークアウトではなく)コンパイラーがそれぞれをチェックするコードを生成する必要があると私はいつも思っていました...しかし、私はテストを実行しただけで、VS2012は少なくとも最初のfalseの後ですべてを短絡するほどスマートでした。私はこれをより頻繁に使用します。 注:を使用するとgoOn &= checkN()checkN()が最初にあった場合でも常に実行goOnfalseifます(つまり、実行しないでください)。
マーク

11
@Nawaz:コードベース全体で恣意的な変更を行う訓練を受けていない心を持っていれば、ifs 内の単なる割り当てよりもはるかに大きな問題があります。
Idelic 2013

6
@sisharpエレガンスは見る人の目の前にあります!私は、世界でループ構造の誤用がどういうわけか「エレガント」であると認識される可能性があることを理解できませんが、おそらくそれは私だけです。
dasblinkenlight 2013

38
  1. コードを別の関数(またはおそらく複数)に抽出してみてください。次に、チェックが失敗した場合に関数から戻ります。

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

  3. プログラムフローが(必要に応じて)非常に波状であり、本当にが必要gotoな場合は、変な変装をするのではなく、それを使用します。

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


4
RAIIは有用ではありますが、特効薬ではないことを謙虚に申し上げます。他に用途のないconvert-goto-to-RAIIクラスを作成しようとしている場合は、すでに述べた「goto-end-of-the-world」イディオムを使用するだけで、より良いサービスが提供されると思います。
idoby 2013

1
@busy_wait:確かに、RAIIはすべてを解決することはできません。それが私の答えが2番目のポイントで止まらない理由ですが、それがgoto本当により良いオプションであるかどうかを提案し続けます。
マイクシーモア

3
同意しますが、goto-RAII変換クラスを作成することは悪い考えであり、明示的に記述する必要があると思います。
idoby 2013

@idoby 1つの RAIIテンプレートクラスを作成し、ラムダで必要な使い捨てクリーンアップを使用してそれをインスタンス化する方法
カレス

@Calethは、あなたが俳優/ドクターを再発明しているように聞こえますか?
idoby

37

TLDRRAII、トランザクションコード(結果が設定されているか、すでに計算されている場合はそれを返す)と例外。

長い答え:

では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ブロック全体が不要になり、クライアントコードで初期の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)、新しいコードに#definegotoラッパーマクロとクリーンアップラベルを追加する必要はありません。他に何か)。


+1。これは私がRAIIか何かを使って使おうとしているものです。shared_ptrカスタム削除機能を使用すると、多くのことができます。C ++ 11のラムダを使用するとさらに簡単になります。
Macke 2013

True、ただし、ポインタではないものにshared_ptrを使用する場合は、少なくともそれをtypedefすることを検討してくださいnamespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };。この場合(make_handle構築時に正しい削除タイプを設定すると)、タイプの名前はもはやポインタであることを示唆しません。
utnapistim 2013

21

完全を期すために答えを追加します。他の多くの回答は、大きな条件ブロックが別の関数に分割される可能性があることを指摘しました。しかし、何度も指摘されたように、このアプローチは条件付きコードを元のコンテキストから分離します。これが、ラムダがC ++ 11の言語に追加された1つの理由です。ラムダの使用は他の人から提案されましたが、明示的なサンプルは提供されていません。この答えに1つ入れました。私を驚かすのは、それがdo { } while(0)多くの点でアプローチに非常に似ていると感じていることです-そしておそらくそれはまだgoto変装していることを意味します...

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

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

7
私にとって、このハックはdo ... whileハックよりも悪く見えます。
マイケル

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 2013

@jamesdlin:スイッチのオープニングブラケットの前にケースを置くと便利なことは、これまでにありませんでした。しかし、実際にはCで許可されており、この場合、優れたマクロを作成すると便利です。
クリス

15

Matsの回答から不要なものを除いたものに似たアプローチをお勧めし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のコメントに注意してください。
2013

2
OPがコードブロックの途中でリソースを取得することについて何か言ったことを覚えていませんが、それを実現可能な要件として受け入れます。その場合、スタック上のリソースをどこかに宣言し、func()そのデストラクタがリソースの解放を処理できるようにすることの何が問題になっていますか?外にあるものfunc()が同じリソースにアクセスする必要がある場合はfunc()、適切なリソースマネージャが呼び出す前にヒープ上で宣言する必要があります。
Dan Bechard、2013

12

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


11

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

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

// edit 
doThingsAtEndAndReportErrorStatus()

2
ただし、各条件には前の条件を含める必要があります。これは醜いだけでなく、パフォーマンスにとって潜在的に悪いものです。「大丈夫」ではないとわかったらすぐに、これらのチェックを飛び越えてクリーンアップすることをお勧めします。
TNE

それは事実ですが、このアプローチは、最後に実行する必要があるため早期に戻りたくない場合に有利です(編集を参照)。条件でブール値を使用するデニスのアプローチと組み合わせると、非常にタイトなループでない限り、パフォーマンスへの影響は無視できます。
the_mandrill 2013

10

dasblinkenlightの回答に似てifいますが、コードレビューアによって「修正」される可能性のある内の割り当てを回避します。

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

...

ステップの結果を次のステップの前にチェックする必要がある場合、このパターンを使用します。これは、すべてのチェックを大きなif( check1() && check2()...型パターンで事前に実行できる状況とは異なります。


check1()は、実際にはステップの結果コードを返すPerformStep1()である可能性があることに注意してください。これにより、プロセスフロー機能の複雑さが抑えられます。
Denise Skidmore 2013

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
本当に?まず、正直なところ、読みやすさの向上は見られません。例外を使用してプログラムフローを制御しないでください。それは彼らの適切な目的ではありません。また、例外によりパフォーマンスが大幅に低下します。例外の適切な使用例は、別のプロセスによって既に排他的にロックされているファイルを開こうとしたり、ネットワーク接続が失敗したり、データベースへの呼び出しが失敗したりするなど、何も実行できない状態が存在する場合です。または、呼び出し元が無効なパラメータをプロシージャに渡しました。そういうこと。ただし、例外を使用してプログラムフローを制御しないでください。
クレイグ

3
つまり、それについて鼻をかむのではなく、あなたが参照したそのStroustrupの記事に行き、「何のために例外を使用すべきでないのか」を探してください。セクション。とりわけ、彼は次のように述べています。 「特に、throwは、関数から値を返す(returnと同様の)単なる代替方法ではありません。そうすることは遅くなり、エラーにのみ使用される例外のシーリングに慣れているほとんどのC ++プログラマーを混乱させます。同様に、スローはループから抜け出すための良い方法ではありません。」
クレイグ

3
@Craigあなたが指摘したことはすべて正しいですが、check()条件が失敗した後もサンプルプログラムが続行できると想定していることは確かであり、それは当然のことです。サンプルにはコンテキストがありません。プログラムが続行できないと仮定すると、例外を使用する方法があります。
Cartucho 2013

2
まあ、それが実際にコンテキストである場合は十分に真実です。しかし、仮定についておかしいこと... ;-)
クレイグ

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ラベルが表示されなかったために誤ってコードを間に配置することで発生する可能性がある1つの問題を回避するためのものです。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、プリプロセッサを使用して言語構文を100万回以上拡張するよりも選択します。
クリスチャン

2
「このコードをよりクリーンでわかりやすくするには、[LOADS_OF_WEIRD_MACROS]を使用できます」:計算しません。
underscore_d

8

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

以下で受け取ったコメントに応じて、ここで紹介を編集しました。C++モナドの実装に関する詳細は、Rotsorの提案を実現できるさまざまな場所にあります。モナドを処理するのには時間がかかるので、代わりに、ここではブースト::オプション以外の何も知らない必要がある「貧乏人」モナドのようなメカニズムを提案します。

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

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
このコードには、個々の関数に前の関数の失敗を強制的に処理させるという望ましくない特性があり、モナドイディオムを適切に利用していません(モナド効果、この場合は失敗は暗黙的に処理されることになっています)。そのためには、optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);代わりに、関数アプリケーションではなくモナディック合成操作(「バインド」)を使用する必要があります。
Rotsor 2013

ありがとう。あなたは正しいです-それはそれを正しく行う方法です。私はそれを説明するために私の答えにあまり多くを書きたくありませんでした(したがって、ここで私が全体を独り占めしていなかったことを示唆することを意図していた「貧乏人」という用語)。
ベネディクト

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

これは可能な場合は便利ですが、ここで尋ねた質問よりも一般的ではありません。すべての条件は、最後の枝切りテストの後に実行されたコードに依存する可能性があります。
クリス、2013

ここでの問題は、原因と結果を分けることです。つまり、同じ条件番号を参照するコードを分離すると、これがさらなるバグの原因になる可能性があります。
リガ

@kriss:まあ、ConditionCode()関数はこれを処理するように調整できます。この関数の重要な点は、最終的な条件が計算されるとすぐに、return <result>を使用してクリーンな終了を行えることです。そして、それがここで構造的な明快さを提供しているものです。
karx11erx 2013

@リガ:イモこれらは完全に学術的な異論です。新しいバージョンでは、C ++がより複雑で不可解になり、読めなくなることがわかりました。小さなヘルパー関数が複雑な条件を適切に構造化された方法で評価し、ターゲット関数をすぐ下で読みやすくする問題は見当たりません。
karx11erx 2013

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 ...
マイケル、

5
マクロに名前を付けるときは、マクロのように(つまり、すべて大文字で)見えるようにする必要があります。そうでなければ、たまたま変数/関数/タイプ/などに名前を付ける人。名前everは非常に不幸になります...
jamesdlin 2013

5

私は特に使用方法にはないですbreakreturn、このようなAの場合には。通常、このような状況に直面している場合、それは通常比較的長い方法です。

複数の出口点がある場合、特定のロジックが実行される原因を知りたいときに問題が発生する可能性があります。通常、そのロジックの一部を囲むブロックを上に移動し続けるだけで、それらを囲むブロックの基準により、状況:

例えば、

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

囲んでいるブロックを見るとmyLogic()conditionA and conditionB and conditionCがtrueの場合にのみ発生することが簡単にわかります。

早期の返品があると、見えにくくなります。

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

から上myLogic()に移動して、囲みブロックを見て条件を理解することはできません。

私が使用したさまざまな回避策があります。以下はその1つです。

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

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

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

(もちろん、すべてを置き換えるために同じ変数を使用することは歓迎されています isA isB isC。)

そのようなアプローチは、少なくともコードの読者に与えるでしょう、それmyLogic()は時に実行されisB && conditionCます。読者には、isBがtrueになる原因をさらに検索する必要があるというヒントが与えられます。


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 2013

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 ++プログラマではないので、ここではコードを記述しませんが、これまでのところ、オブジェクト指向のソリューションについて言及した人はいません。だからここに私の推測があります:

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

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

ただし、欠点は、リストでのオブジェクトの使用数と反復回数が多いため、オーバーヘッドが若干増えることです。


別の言語の例を追加できますか?この質問は、C ++について具体的に尋ねられましたが、多くの言語に当てはまると思います。
Denise Skidmore 2013

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)

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

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

これらのセマンティクスを取得する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;
}

...等々。「do」コードの横に「undo」コードを配置するため、これは監査が簡単です。簡単な監査が良いです。また、制御フローが非常に明確になります。Cにとっても便利なパターンです。

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


クリーンアップパスの監査は非常に簡単ですが、ゴールデンパスの追跡はそれほど簡単ではないかもしれません。しかし、全体として、このようなものは一貫したクリーンアップパターンを促進すると思います。
Denise Skidmore 2013

0

コードにif..else if..elseステートメントの長いブロックがある場合、Functorsまたはの助けを借りてブロック全体を書き直すことができますfunction pointers。常に適切な解決策とは限りませんが、多くの場合そうです。

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


原則としてこれは可能です(悪くはありません)が、明示的な関数オブジェクトまたはポインターを使用すると、コードフローを非常に強く中断するだけです。OTOH、ここでは同等のもの、ラムダまたは通常の名前付き関数の使用は、実践的で効率的でよく読みます。
leftaroundabout '09

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

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

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

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


2
これは、条件テストの間に何かを行う必要がない場合にのみ機能します。(まあ、私はあなたが条件テストによって行われたいくつかの関数呼び出しの中に何かを隠すことができると思いますが、それをやり過ぎると少し不明瞭になるかもしれません)
Jeremy Friesner '29

@JeremyFriesner実際には、実際には、常にと評価される個別のブール関数として中間の処理を行うことができますtrue。短絡評価は、すべての前提条件テストに合格しなかった中間のものが決して実行されないことを保証します。
AJMansfield 2013

@AJMansfieldはい、それは私が2番目の文で言及していたものです...しかし、それを行うことでコードの品質が向上するかどうかはわかりません。
Jeremy Friesner、2013

@JeremyFriesner条件をとして書くことを妨げるものは何もありません(/*do other stuff*/, /*next condition*/)。うまくフォーマットすることもできます。人々がそれを好きになることを期待しないでください。しかし、正直なところ、これが唯一のJavaがいることを段階的に廃止することが間違いだったことを示すことを行くgotoの文...
cmasterは-モニカ復活

@JeremyFriesnerこれらはブール値であると想定していました。関数が各条件の中で実行される場合、それを処理するためのより良い方法はありません。
Tyzoid 2013

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.