当代码流是这样的:
if(check())
{
...
...
if(check())
{
...
...
if(check())
{
...
...
}
}
}
我通常看到这种方法可以避免上述混乱的代码流:
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
有什么更好的方法可以避免这种工作/黑客,从而使其成为更高级别(行业级别)的代码?
任何跳出盒子的建议都是欢迎的!
首先,一个简短的例子来说明为什么goto不是一个好的c++解决方案:
struct Bar {
Bar();
};
extern bool check();
void foo()
{
if (!check())
goto out;
Bar x;
out:
}
试着把它编译成一个object文件,看看会发生什么。然后尝试等价的do+ break + while(0)。
这是题外话。主要观点如下。
如果整个函数失败,这些小块的代码通常需要某种类型的清理。当您“展开”部分完成的计算时,这些清理通常希望以与块本身相反的顺序发生。
获得这些语义的一个选项是RAII;参见@utnapistim的回答。c++保证自动析构函数以与构造函数相反的顺序运行,这自然提供了一个“unwind”。
但这需要大量的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函数接受大量参数,但如果您的类/结构具有良好的内聚性,这通常不是问题。(也就是说,属于一起的东西生活在一个对象中,所以这些函数可以作为指向少量对象的指针或引用,但仍然可以做很多有用的工作。)
从函数式编程的角度来看,这是一个众所周知的、很好解决的问题——也许是单子。
为了回应下面我收到的评论,我在这里编辑了我的介绍:你可以在不同的地方找到实现c++单子的完整细节,这将让你实现Rotsor建议的目标。这需要一段时间来理解单子,所以我要在这里建议一个快速的“穷人”单子式机制,你只需要知道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或类似的方法来提出一些变化。
我不是特别喜欢在这种情况下使用break或return的方法。考虑到通常当我们面对这样的情况时,这通常是一个比较长的方法。
如果我们有多个出口点,当我们想要知道什么会导致某些逻辑被执行时,这可能会造成困难:通常情况下,我们只是继续向上包含该逻辑的块,而这些块的标准告诉我们情况:
例如,
if (conditionA) {
....
if (conditionB) {
....
if (conditionC) {
myLogic();
}
}
}
通过查看封闭块,很容易发现myLogic()只在条件a和条件b和条件c为真时发生。
当有早期的回报时,它变得不那么明显:
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)
这样的方法至少会让代码的读者知道,当isB && conditionC时执行myLogic()。给读者一个提示,他需要进一步查找使isB为真的原因。