在c++中执行多个相关错误检查的优雅方式,不需要goto,也不需要提前返回
Elegant way to perform multiple dependent error checks in C++ without goto, and without early return?
假设我想连续调用四个函数,它们对我的某个对象进行操作。如果它们中的任何一个失败,我想返回FAILURE
而不调用其他的,如果它们都成功完成,我想返回SUCCESS
。
通常,我会这样做:
if(function_zero(&myMutableObject) == SUCCESS)
{
return FAILURE;
}
if(function_one(&myMutableObject) == SUCCESS)
{
return FAILURE;
}
if(function_two(&myMutableObject) == SUCCESS)
{
return FAILURE;
}
if(function_three(&myMutableObject) == SUCCESS)
{
return FAILURE;
}
return SUCCESS;
或者,如果我需要做一些清理:
if(function_zero(&myMutableObject) == SUCCESS)
{
status = FAILURE;
goto cleanup;
}
if(function_one(&myMutableObject) == SUCCESS)
{
status = FAILURE;
goto cleanup;
}
if(function_two(&myMutableObject) == SUCCESS)
{
status = FAILURE;
goto cleanup;
}
if(function_three(&myMutableObject) == SUCCESS)
{
status = FAILURE;
goto cleanup;
}
cleanup:
// necessary cleanup here
return status;
然而,我正在做的项目有一些限制:
- No
goto
, ever - 不提前返回(每个函数一次返回)
- 线长限制
- (EDIT)无例外
- (EDIT)没有模板。
这使我得出这样的结论:
if(function_zero(&myMutableObject) == SUCCESS)
{
if(function_one(&myMutableObject) == SUCCESS)
{
if(function_two(&myMutableObject) == SUCCESS)
{
status = function_three(&myMutableObject);
}
else
{
status = FAILURE;
}
}
else
{
status = FAILURE;
}
}
else
{
status = FAILURE;
}
return status;
不幸的是,这经常使我超出行长限制。
我的问题是:有没有更简单的方法来写这个?
指出,限制:
- 我必须在这里隐含的代码块中实现这个逻辑。我不能创建新的功能,重构或改变整体架构。
- (编辑)在现实中,函数有非常不同的签名。
使用异常和RAII。这些都是它们被发明出来要解决的问题。然而,异常更多的是一个系统范围的特性,而不是你可以在本地应用的东西。
对于清理块,RAII正是您所需要的特性。
对于成功/失败,我们可以使用lambdas和variadics将它们隐式地链接在一起。
现在我们可以简单地将它们写成列表中的lambda。
status f() {
struct nested {
static template<typename F> status_t Check(F f) {
return f();
}
static template<typename F, typename... Chain> status_t Check(F f, Chain... chain) {
auto status = f();
return status != failure ? Check(chain...) : status;
}
};
return nested::Check(
[] { return function_zero(&myMutableObject); },
[] { return function_one(&myMutableObject); },
[] { return function_two(&myMutableObject); },
[] { return function_three(&myMutableObject); },
);
}
如果您需要捕获返回值,这会变得稍微更有问题,但是由于它似乎总是带有参数的错误代码,如果您只是在f()
中声明接收变量应该是好的,那么所有未来的lambda都可以引用它。它也不要求每个函数都有相同的签名,或者分配不同的数据结构。
仅在当前使用&&操作符。如果设置为FAILURE,则&&将立即失败,并且不会执行后续测试。
status = SUCCESS
if (status == SUCCESS && function_zero(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
if (status == SUCCESS && function_one(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
if (status == SUCCESS && function_two(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
if (status == SUCCESS && function_three(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
return status;
正如@Mooing Duck建议的那样,您可以简单地在else if链中完成所有操作:
status = SUCCESS
if (function_zero(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
else if (function_one(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
else if (function_two(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
else if (function_three(&myMutableObject) == FAILURE)
{
status = FAILURE;
}
return status;
我在处理长链C调用(通常是winapi)时使用过几次的模式是这样的:
bool ret =
function_zero(&myMutableObject) == SUCCESS
&&
function_one(&myMutableObject) == SUCCESS
&&
function_two(&myMutableObject) == SUCCESS
&&
function_three(&myMutableObject) == SUCCESS;
if(!ret)
{
// cleanup
}
return ret?SUCCESS:FAILURE;
您甚至可以在每行的末尾留下&&
,这样它看起来更像一个"正常"的调用序列(尽管我个人更喜欢这样,它更清楚发生了什么)。
&&
操作符保证以正确的顺序执行,并且只有在前面的调用成功的情况下才执行,并在调用之间引入必要的序列点(或者在c++ 11中如何调用它们),因此在各种调用之间计算参数的顺序是很好的定义的。而且,它的优先级足够低,不需要额外的括号。
如果您不害怕使用com风格的宏,您还可以将== SUCCESS
检查封装在宏中,如
// in some header, maybe with a less abused name
#define OK(x) ((x) == SUCCESS)
bool ret =
OK(function_zero(&myMutableObject))
&&
OK(function_one(&myMutableObject))
&&
OK(function_two(&myMutableObject))
&&
OK(function_three(&myMutableObject));
// ...
更好的是,如果SUCCESS != 0
和FAILURE == 0
,你可以完全放弃OK()
和== SUCCESS
,只使用&&
来连接调用。
你说没有例外,但我认为你应该知道你放弃了什么。
如果您使用基于RAII的清理并将错误报告为异常而不是代码,那么您将得到如下代码:
function_zero(&myMutableObject);
function_one(&myMutableObject);
function_two(&myMutableObject);
function_three(&myMutableObject);
这是一个解释正确的c++异常处理及其好处的站点:
http://exceptionsafecode.com/
这些好处包括:
- 更容易阅读
- 更容易理解和维护
- 代码错误
- 更容易编写
- 在成功路径上改进了性能
- <
- "零成本"异常/gh>
- 编译器将异常理解为一种语言特性,知道哪条路径是成功路径,哪条路径是失败路径
- 异常表的代码大小增加通过消除错误检查代码 来抵消
此外,Herb Sutter对"单次退出"(你的"不提前返回"规则的通用名称)有这样的看法:
http://herbsutter.com/category/c/gotw/page/4/一般来说,请注意SE/SE是一个过时的想法,并且一直都是错了。"单条目",或者函数应该总是这样在一个地方进入(在他们的开始),而不是用goto从跳跃调用者的代码直接到函数体内的随机位置,是这是计算机科学中非常有价值的进步。这就是使库成为可能,因为它意味着你可以打包一个函数并重用它,函数总是知道它的起始状态,它开始的地方,不管调用代码是什么。"单一出口,"另一方面,在优化的基础上得到了不公平的普及如果有一个单一的返回,编译器可以执行返回值优化更好"(见上面的反例)和对称性("如果单一入口是好的,单一出口也必须是好的’),但这是错误的因为这些原因不能反向保持——允许调用者插入是坏的,因为它不在函数的控制之下,但允许函数本身在知道它已经完成的时候提前返回是完美的精细,完全在功能控制之下。
您应该尝试更新规则,即使这只可能用于新项目。
可以将函数指针存储在std::function的容器中(只要它们具有与示例中相同的签名):
std::vector<std::function<error_code (myobject&)> functions { func1, func2, func3, func4 };
error_code status = SUCCESS;
for (const auto& f : functions) {
if (f(myobject) == ERROR) {
clean_up();
status = ERROR;
break;
}
}
return status;
这可以工作:
bool success = true;
success = success && function_zero(&myMutableObject) != FAILURE;
success = success && function_one(&myMutableObject) != FAILURE;
success = success && function_two(&myMutableObject) != FAILURE;
success = success && function_three(&myMutableObject) != FAILURE;
return success ? SUCCESS : FAILURE;
return可以替换为:
int status = SUCCESS;
if( !success ) status = FAILURE;
return status;
if在您的公司也被禁止使用条件运算符。
你可以写一个简单的局部函子:
bool fn0(int&) { return false; }
bool fn1(int&) { return false; }
bool fn2(int&) { return false; }
int main() {
struct Test {
const bool result;
typedef bool (*function)(int&);
Test(function f, int& value)
: result(f(value))
{};
operator bool () const { return result; }
};
int value;
return Test(fn0, value)
&& Test(fn1, value)
&& Test(fn2, value);
}
- 在两个类中共享相同的函数调用,并在不需要时避免空实例化
- 是否有类似std::lower_bound的函数,而不需要排序/分区输入
- 为什么output_editor Concept不需要output_e迭代器标记
- 在除法中不需要四舍五入
- 与C代码相比,为什么C++代码不需要"#define _POSIX_C_SOURCE 200809L"?
- 将值从另一个数组写入数组,不包括不需要的值 C++
- C++ 写入路径名中包含不需要的空字符的文件
- 是否有必要获取锁并在不需要唤醒线程时通知condition_variable?
- 使用 assimp 加载模型 - 不需要提升?
- 为什么转换函数声明不需要至少一个定义类型说明符
- 返回不需要的值的二叉搜索程序
- 当我不需要数据库中的所有值时,如何部分初始化 c++ 对象?
- C++:用户输入会产生不需要的行为
- 如何在不需要LIBCD.lib的情况下在Visual Studio 6中编译C项目
- 为什么python需要全局关键字而C/C++不需要?
- 将一个宏传递到另一个宏而不是直接传递内容时会出现不需要的额外"空"参数
- 为什么我们需要在 C++ 中检查空指针,而在 Java 中不需要?
- 一种方式链接列表和不需要的第一个元素AFRTER填充
- 如果我们使用链表数组来实现哈希表,则可以以不需要遍历的方式实现"add"。这是真的还是假的?
- 在c++中执行多个相关错误检查的优雅方式,不需要goto,也不需要提前返回