コードフローが次のような場合:
if(check())
{
...
...
if(check())
{
...
...
if(check())
{
...
...
}
}
}
上記の厄介なコードフローを回避するために、この回避策を一般的に見てきました。
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
この回避策/ハッキングを回避して、より高いレベル(業界レベル)のコードになるためのより良い方法は何ですか?
すぐに使える提案は大歓迎です!
関数でこれらの決定を分離し、return
sの代わりにbreak
sを使用することは受け入れられる慣行と考えられます。これらのチェックはすべて、関数と同じレベルの抽象化に対応していますが、非常に論理的なアプローチです。
例えば:
void foo(...)
{
if (!condition)
{
return;
}
...
if (!other condition)
{
return;
}
...
if (!another condition)
{
return;
}
...
if (!yet another condition)
{
return;
}
...
// Some unconditional stuff
}
goto
を使用することが実際に正しい答えである場合があります-少なくとも、「goto
はどんな質問であっても決して答えにならない」という宗教的信念に育てられていない人には-これはそのようなケースの1つです。
このコードは、goto
をbreak
としてドレスアップすることのみを目的として、do { ... } while(0);
のハックを使用しています。 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
が他のソリューションよりも優れたソリューションである場合があります。
たとえば、いくつかのデータを収集しており、テスト対象のさまざまな条件は、「これが収集されるデータの終わり」であると想像してください。これは、場所によって異なる「継続/終了」マーカーあなたはデータストリームにいます。
さて、完了したら、データをファイルに保存する必要があります。
そして、はい、多くの場合、常にではありませんが、合理的なソリューションを提供できる他のソリューションがあります。
bool
変数を使用して、単純な継続パターンを使用できます。
bool goOn;
if ((goOn = check0())) {
...
}
if (goOn && (goOn = check1())) {
...
}
if (goOn && (goOn = check2())) {
...
}
if (goOn && (goOn = check3())) {
...
}
この実行チェーンは、checkN
がfalse
を返すとすぐに停止します。 &&
演算子の短絡により、これ以上check...()
呼び出しは実行されません。さらに、最適化コンパイラは、goOn
をfalse
に設定することは一方通行であることを認識し、欠落しているgoto end
を挿入するのに十分スマートです。その結果、上記のコードのパフォーマンスはdo
/while(0)
のパフォーマンスと同じになりますが、その可読性に痛みを伴う打撃はありません。
コードを別の関数(または複数の関数)に抽出してみてください。次に、チェックが失敗した場合、関数から戻ります。
それを行うには周囲のコードと密に結合しすぎており、結合を減らす方法が見つからない場合は、このブロックの後のコードを見てください。おそらく、関数によって使用されるいくつかのリソースをクリーンアップします。 RAII オブジェクトを使用してこれらのリソースを管理してみてください。次に、各危険なbreak
をreturn
(または、より適切な場合はthrow
)に置き換え、オブジェクトのデストラクターをクリーンアップします。
プログラムの流れが(必然的に)波状で、本当にgoto
が必要な場合は、変な変装を与えるのではなく、それを使用してください。
goto
を盲目的に禁止するコーディングルールがあり、プログラムフローを実際に単純化できない場合、おそらくdo
ハックでそれを隠す必要があります。
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 != ...
および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)
を追加する必要はありません。また、 #define
gotoラッパーマクロとクリーンアップラベルなど。
確かに答えではなく、答え(完全を期すために) )
の代わりに :
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;
...
...
}
これはまだgotoに変装していますが、少なくともループではありません。つまり、ブロックのどこかにcontinueが隠されていないかどうかを非常に慎重に確認する必要はありません。
コンストラクトも十分に単純なので、コンパイラーが最適化することを期待できます。
@jamesdlinが示唆するように、次のようなマクロの後ろにそれを隠すこともできます。
#define BLOC switch(0) case 0:
そして、それを次のように使用します
BLOC {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
}
これが可能なのは、C言語の構文がスイッチの後のステートメントであり、括弧で囲まれたブロックではなく、そのステートメントの前にケースラベルを置くことができるためです。今まではそれを許可する意味がわかりませんでしたが、この特定のケースでは、スイッチをNiceマクロの後ろに隠すのが便利です。
不要なgoto
を除いたMatsの回答に似たアプローチをお勧めします。関数に条件付きロジックのみを配置します。常に実行されるコードは、呼び出し元で関数が呼び出される前または後に移動する必要があります。
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;
...
}
私にとって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
を使用せずにスパゲッティコードを生成できます。
コードフロー自体は、既にコードの匂いがしており、関数内で多くのことが発生しています。それに対する直接的な解決策がない場合(関数は一般的なチェック関数です)、 RAII を使用して、関数の最後のセクションにジャンプする代わりに戻ることができます。
実行中にローカル変数を導入する必要がない場合、多くの場合、これをフラット化できます。
if (check()) {
doStuff();
}
if (stillOk()) {
doMoreStuff();
}
if (amIStillReallyOk()) {
doEvenMore();
}
// edit
doThingsAtEndAndReportErrorStatus()
Dasblinkenlightの答えに似ていますが、コードレビューアによって「修正」される可能性のあるif
内の割り当てを回避します。
bool goOn = check0();
if (goOn) {
...
goOn = check1();
}
if (goOn) {
...
goOn = check2();
}
if (goOn) {
...
}
...
次のステップの前にステップの結果を確認する必要がある場合にこのパターンを使用します。これは、大きなif( check1() && check2()...
タイプパターンですべてのチェックを事前に実行できる状況とは異なります。
例外を使用します。コードはよりきれいに見えます(プログラムの実行フローでエラーを処理するために例外が作成されました)。リソース(ファイル記述子、データベース接続など)をクリーンアップするには、記事を読んでください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;
}
これは、関数型プログラミングの観点からはよく知られ、解決された問題です-多分モナドです。
以下で受け取ったコメントに応じて、ここで紹介を編集しました: C++ モナドの実装に関する詳細は さまざまな場所 で見つけることができます。 。モナドを理解するにはしばらく時間がかかるので、代わりに、boost :: optional以外のことを知る必要のない、「poor-mans」モナドのような簡単なメカニズムをここで提案します。
次のように計算手順を設定します。
boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);
各計算ステップは、指定されたオプションが空の場合、明らかにboost::none
を返すなどの処理を実行できます。たとえば、次のとおりです。
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などを使用してさまざまなバリエーションを見つけることができるたびに同じオブジェクトを返す方が便利な場合。
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;
}
}
おそらくこのようなもの
#define EVER ;;
for(EVER)
{
if(!check()) break;
}
または例外を使用する
try
{
for(;;)
if(!check()) throw 1;
}
catch()
{
}
例外を使用して、データを渡すこともできます。
そのような場合、break
またはreturn
を使用する方法には特に興味がありません。通常、このような状況に直面している場合、通常は比較的長い方法です。
複数の出口点がある場合、特定のロジックが実行される原因を知りたい場合、困難が生じる可能性があります:通常、そのロジックの一部を囲むブロックを続けていくだけで、ブロックを囲むそれらの基準は状況:
例えば、
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
を置き換えるために同じ変数を使用することを歓迎します。)
そのようなアプローチは、少なくともコードの読者に、isB && conditionC
のときにmyLogic()
が実行されるようにします。読者には、isBがtrueになる原因をさらに調べる必要があるというヒントが与えられます。
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++;
}
どのようにそのことについて?
私は C++ プログラマーではないので、ここではコードを記述しませんが、これまでのところ、オブジェクト指向ソリューションについて言及した人はいません。だからここに私の推測があります:
単一の条件を評価するためのメソッドを提供する汎用インターフェイスがあります。これで、問題のメソッドを含むオブジェクトでこれらの条件の実装のリストを使用できます。リストを反復処理し、各条件を評価します。条件が失敗した場合は、早めにブレークアウトします。
メソッドを含むオブジェクトの初期化中に新しい条件を簡単に追加できるため、このような設計は open/closed原則 に非常によく準拠しているのが良い点です。問題です。条件の説明を返す条件評価のメソッドを使用して、インターフェイスに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;
}
これが私のやり方です。
void func() {
if (!check()) return;
...
...
if (!check()) return;
...
...
if (!check()) return;
...
...
}
最初に、goto
がC++の良い解決策ではない理由を示す短い例:
struct Bar {
Bar();
};
extern bool check();
void foo()
{
if (!check())
goto out;
Bar x;
out:
}
これをオブジェクトファイルにコンパイルして、何が起こるかを確認してください。次に、同等のdo
+ break
+ while(0)
を試してください。
それは余談でした。主なポイントは次のとおりです。
これらの小さなコードチャンクでは、関数全体が失敗した場合にsome種類のクリーンアップが必要になることがよくあります。これらのクリーンアップは通常、部分的に終了した計算を「巻き戻す」ときに、チャンク自体から反対の順序で行われます。
これらのセマンティクスを取得する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
関数が多くの引数を取る必要がある場合がありますが、クラス/構造体に適切な凝集性がある場合、通常は問題になりません。 (つまり、一緒に属するものは単一のオブジェクトに存在するため、これらの関数は少数のオブジェクトへのポインターまたは参照を取得し、それでも多くの有用な作業を実行できます。)
コードにif..else if..elseステートメントの長いブロックがある場合は、Functors
またはfunction pointers
を使用してブロック全体を書き換えてみてください。それは常に正しい解決策ではないかもしれませんが、かなり頻繁にそうです。
http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html
すべてのエラーに同じエラーハンドラを使用し、各ステップが成功を示すブール値を返す場合:
if(
DoSomething() &&
DoSomethingElse() &&
DoAThirdThing() )
{
// do good condition action
}
else
{
// handle error
}
(tyzoidの答えに似ていますが、条件がアクションであり、&&は最初の失敗後に追加のアクションが発生するのを防ぎます。)
フラグを立てる方法が答えられなかった理由は、古くから使用されています。
//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
}
1つのif
ステートメントに統合します。
if(
condition
&& other_condition
&& another_condition
&& yet_another_condition
&& ...
) {
if (final_cond){
//Do stuff
} else {
//Do other stuff
}
}
これは、gotoキーワードが削除されたJavaなどの言語で使用されるパターンです。
ここで提示されているさまざまな答えに驚かされます。しかし、最終的に私が変更しなければならないコード(つまり、この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;
}
...
...
そのため、ここで行われたことは、機能の仕上げが孤立し、物事が突然非常にシンプルでクリーンになったことです!
私はこの解決策に言及する価値があると思ったので、ここで提供しました。