当代码流如下:
if(check())
{
...
...
if(check())
{
...
...
if(check())
{
...
...
}
}
}
我一般都看到这个工作,以避免上面的混乱代码流:
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
有哪些更好的方法可以避免这种解决方法/黑客攻击,从而使其成为更高级别(行业级别)的代码?
欢迎任何开箱即用的建议!
答案 0 :(得分:307)
在函数中隔离这些决策并使用return
而不是break
s被视为可接受的做法。虽然所有这些检查都对应于与函数相同的抽象级别,但这是非常合乎逻辑的方法。
例如:
void foo(...)
{
if (!condition)
{
return;
}
...
if (!other condition)
{
return;
}
...
if (!another condition)
{
return;
}
...
if (!yet another condition)
{
return;
}
...
// Some unconditional stuff
}
答案 1 :(得分:252)
有时候使用goto
实际上是正确的答案 - 至少对那些没有在宗教信仰中提出“goto
永远不能成为答案的人来说,无论问题是什么是“ - 这是其中一个案例。
此代码使用do { ... } while(0);
的黑客攻击的唯一目的是将goto
打扮成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
是比其他解决方案更好的解决方案。
想象一下,例如我们正在收集一些数据,并且正在测试的不同条件是某种“这是收集的数据的结束” - 这取决于某种“持续/结束”标记,这些标记不同取决于您在数据流中的位置。
现在,当我们完成后,我们需要将数据保存到文件中。
是的,通常有其他解决方案可以提供合理的解决方案,但并非总是如此。
答案 2 :(得分:82)
您可以使用带有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)
的代码相同,只是没有给它的可读性带来痛苦的打击。
答案 3 :(得分:37)
尝试将代码提取到单独的函数中(或者可能不止一个)。如果检查失败,则从函数返回。
如果它与周围的代码紧密耦合,并且您无法找到减少耦合的方法,请查看此块之后的代码。据推测,它清理了函数使用的一些资源。尝试使用RAII对象管理这些资源;然后用break
(或return
替换每个狡猾的throw
,如果这更合适的话),让对象的析构函数为你清理。
如果程序流程(必然)如此波动以至于您确实需要goto
,那么请使用它而不是给它一个奇怪的伪装。
如果您的编码规则盲目禁止goto
,并且您实际上无法简化程序流程,那么您可能需要使用do
hack来伪装它。 / p>
答案 4 :(得分:36)
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)
在新代码中,也不是#define
goto包装器宏和清理标签,也不是其他任何内容。
答案 5 :(得分:20)
为了完整起见,我正在添加一个答案。许多其他答案指出,大条件块可以分成单独的函数。但正如同时也指出的那样,这种方法将条件代码与原始上下文分开。这是在C ++ 11中将lambdas添加到语言中的一个原因。其他人建议使用lambdas,但没有提供明确的样本。我在这个答案中加了一个。让我感到震惊的是,它在许多方面与do { } while(0)
方法非常相似 - 也许这意味着它仍然是伪装的goto
......
earlier operations
...
[&]()->void {
if (!check()) return;
...
...
if (!check()) return;
...
...
if (!check()) return;
...
...
}();
later operations
答案 6 :(得分: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;
...
...
}
这仍然是伪装的 goto ,但至少它不再是循环了。这意味着你不必非常仔细地检查是否有一些继续隐藏在块中的某个地方。
该构造也很简单,您可以希望编译器将其优化掉。
正如@jamesdlin所建议的那样,你甚至可以隐藏在像
这样的宏之后#define BLOC switch(0) case 0:
并像
一样使用它BLOC {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
}
这是可能的,因为C语言语法需要在切换之后的语句,而不是括号内的块,并且您可以在该语句之前放置一个case标签。到目前为止,我没有看到允许这一点,但在这种特殊情况下,将开关隐藏在漂亮的宏后面是很方便的。
答案 7 :(得分: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;
...
}
答案 8 :(得分:12)
对我来说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)
Spaghetti代码不是goto
的错,这是程序员的错。您仍然可以在不使用goto
的情况下生成意大利面条代码。
答案 9 :(得分:12)
代码流本身已经是代码气味,在函数中发生了很多事情。如果没有直接解决方案(该函数是一般检查函数),那么使用RAII这样你可以返回而不是跳转到函数的结尾部分可能会更好。
答案 10 :(得分:11)
如果您不需要在执行期间引入局部变量,那么您通常可以将其展平:
if (check()) {
doStuff();
}
if (stillOk()) {
doMoreStuff();
}
if (amIStillReallyOk()) {
doEvenMore();
}
// edit
doThingsAtEndAndReportErrorStatus()
答案 11 :(得分:10)
与dasblinkenlight的答案类似,但避免了if
内部可能被代码审核者“修复”的分配:
bool goOn = check0();
if (goOn) {
...
goOn = check1();
}
if (goOn) {
...
goOn = check2();
}
if (goOn) {
...
}
...
当在下一步之前需要检查步骤的结果时,我使用此模式,这与所有检查都可以使用大if( check1() && check2()...
类型模式预先完成的情况不同。
答案 12 :(得分:9)
使用例外。您的代码看起来会更清晰(并且为了处理程序执行流程中的错误而创建了异常)。要清理资源(文件描述符,数据库连接等),请阅读文章 Why doesn't C++ provide a "finally" construct? 。
#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;
}
答案 13 :(得分:8)
从功能编程的角度来看,这是一个众所周知且解决得很好的问题 - 也许是monad。
为了回应我在下面收到的评论,我在这里编辑了我的介绍:您可以在C++中找到有关实施various places monad的完整详细信息,这将使您能够实现Rotsor的建议。这需要一段时间来修复monad所以相反我会在这里建议一个快速的“穷人”monad-like机制,你只需要知道boost :: optional。
设置计算步骤如下:
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等提出一些变化时返回同一个对象会更方便。
答案 14 :(得分: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;
}
}
答案 15 :(得分: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为真的内容。
答案 16 :(得分:5)
这样的事可能
#define EVER ;;
for(EVER)
{
if(!check()) break;
}
或使用例外
try
{
for(;;)
if(!check()) throw 1;
}
catch()
{
}
使用例外,您也可以传递数据。
答案 17 :(得分: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++;
}
那怎么样?
答案 18 :(得分:2)
我不是C++程序员,所以我不会在这里写任何代码,但到目前为止还没有人提到过面向对象的解决方案。所以这是我的猜测:
有一个通用接口,提供评估单个条件的方法。现在,您可以在包含相关方法的对象中使用这些条件的实现列表。您遍历列表并评估每个条件,如果一个失败,可能会提前爆发。
好处是这样的设计非常适合open/closed principle,因为您可以在包含相关方法的对象初始化期间轻松添加新条件。您甚至可以使用条件评估方法向界面添加第二种方法,以返回条件的描述。这可以用于自我文档系统。
然而,缺点是由于使用了更多对象和列表上的迭代,所涉及的开销略有增加。
答案 19 :(得分: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;
}
答案 20 :(得分:1)
首先,举一个简短的例子来说明为什么goto
不是C ++的好解决方案:
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;
}
......等等。这很容易审核,因为它将“撤消”代码放在“执行”代码旁边。轻松审核很好。它还使控制流程非常清晰。它也是C的有用模式。
它可能需要calc
函数来获取大量参数,但如果您的类/结构具有良好的内聚性,那通常不是问题。 (也就是说,属于一起的东西存在于一个对象中,因此这些函数可以获取指针或对少量对象的引用,并且仍然可以进行大量有用的工作。)
答案 21 :(得分:1)
这就是我这样做的方式。
void func() {
if (!check()) return;
...
...
if (!check()) return;
...
...
if (!check()) return;
...
...
}
答案 22 :(得分:0)
将其合并为一个if
声明:
if(
condition
&& other_condition
&& another_condition
&& yet_another_condition
&& ...
) {
if (final_cond){
//Do stuff
} else {
//Do other stuff
}
}
这是在Java等语言中使用的模式,其中删除了goto关键字。
答案 23 :(得分:0)
如果您的代码包含if..else if..else语句的长块,您可以尝试在Functors
或function pointers
的帮助下重写整个块。它可能不是总是正确的解决方案,但经常是。
http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html
答案 24 :(得分:0)
我对这里提出的不同答案的数量感到惊讶。但是,最后在我必须更改的代码中(即删除此do-while(0)
hack或任何东西),我做了一些与此处提到的任何答案不同的东西,我很困惑为什么没有人想到这一点。这是我做的:
初始代码:
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;
}
...
...
所以,这里所做的就是整理过程中的东西已被隔离在一个函数中,事情突然变得如此简单和干净!
我认为这个解决方案值得一提,所以在这里提供。
答案 25 :(得分:0)
如果对所有错误使用相同的错误处理程序,并且每个步骤都返回一个指示成功的bool:
if(
DoSomething() &&
DoSomethingElse() &&
DoAThirdThing() )
{
// do good condition action
}
else
{
// handle error
}
(类似于tyzoid的答案,但条件是行动,&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp; amp;&amp;&amp; amp;&amp;&amp; amp;&amp;
答案 26 :(得分: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
}