通常你有一个函数,对于给定的参数,它不能生成有效的结果,或者它不能执行某些任务。除了在C / C ++世界中不常用的例外情况外,基本上有两所学校报告无效结果。
第一种方法将有效返回与不属于函数的codomain的值混合(通常为-1)并指示错误
int foo(int arg) {
if (everything fine)
return some_value;
return -1; //on failure
}
第二种方法是返回一个函数状态并将结果传递给引用
bool foo(int arg, int & result) {
if (everything fine) {
result = some_value;
return true;
}
return false; //on failure
}
您喜欢哪种方式?为什么?第二种方法中的附加参数是否会带来显着的性能开销?
答案 0 :(得分:14)
对于异常和意外错误,请不要忽略异常。
然而,只是回答你的观点,问题最终是主观的。关键问题是考虑让消费者更容易使用什么,同时悄悄地推动他们记住检查错误状况。在我看来,这几乎总是“返回一个状态代码,并将值放在一个单独的引用中”,但这完全是一个人的个人观点。我这样做的论点......
返回混合错误代码/值模型的参数可能很简单 - 没有额外的变量浮动,例如。但对我来说,危险比有限的收益更糟糕 - 人们很容易忘记检查错误代码。这是例外的一个论点 - 你实际上不会忘记处理它们(如果你不这样做,你的程序就会熄火)。
答案 1 :(得分:8)
boost optional是一项出色的技术。一个例子将有所帮助。
假设您有一个返回double的函数,并且您想要表示 无法计算时出错。
double divide(double a, double b){
return a / b;
}
在b为0的情况下该怎么做;
boost::optional<double> divide(double a, double b){
if ( b != 0){
return a / b;
}else{
return boost::none;
}
}
如下所示使用它。
boost::optional<double> v = divide(a, b);
if(v){
// Note the dereference operator
cout << *v << endl;
}else{
cout << "divide by zero" << endl;
}
答案 2 :(得分:6)
当您开始使用模板时,特殊返回值的想法完全崩溃了。考虑:
template <typename T>
T f( const T & t ) {
if ( SomeFunc( t ) ) {
return t;
}
else { // error path
return ???; // what can we return?
}
}
在这种情况下,我们无法返回明显的特殊值,因此抛出异常实际上是唯一的方法。返回必须检查的布尔类型并通过引用传递真正有趣的值会导致可怕的编码风格..
答案 3 :(得分:4)
相当多的书籍等强烈建议第二,所以你不是要混合角色,强迫返回值带有两个完全不相关的信息。
虽然我同情这个概念,但我发现第一个通常在实践中效果更好。一个明显的观点是,在第一种情况下,您可以将分配链接到任意数量的收件人,但在第二种情况下,如果您需要/想要将结果分配给多个收件人,则必须进行调用,然后单独执行第二个任务。即,
account1.rate = account2.rate = current_rate();
VS:
set_current_rate(account1.rate);
account2.rate = account1.rate;
或:
set_current_rate(account1.rate);
set_current_rate(account2.rate);
布丁的证据就在于吃。微软的COM功能(仅举一例)专门选择了后者的形式。 IMO,主要是由于这个决定,基本上所有代码直接使用本机COM API是丑陋的,几乎不可读。所涉及的概念并不是特别困难,但界面的风格几乎在每种情况下都应该将简单的代码变成几乎难以理解的混乱。
异常处理通常是 处理事物的更好方法。它有三个特定的效果,所有这些都非常好。首先,它使主流逻辑不受错误处理的污染,因此代码的真实意图更加清晰。其次,它将错误处理与错误检测分离。检测到问题的代码通常处于不良位置,以便处理该错误。第三,不同于返回错误的任何一种形式,基本上不可能简单地忽略抛出的异常。有了返回代码,就会有一种几乎不变的诱惑(程序员经常屈服)简单地假设成功,并且不会尝试甚至捕获问题 - 特别是因为程序员并不真正知道如何处理错误无论如何,代码的一部分,并且很清楚即使他捕获它并从函数中返回错误代码,但无论如何 将被忽略。
答案 4 :(得分:2)
在C中,我看到的一种常见技术是函数在成功时返回零,在出错时返回非零(通常是错误代码)。如果函数需要将数据传递回调用者,则它通过作为函数参数传递的指针来完成。这也可以使返回多个数据的函数更容易使用(相对于通过返回值返回一些数据,而通过指针返回一些数据)。
我看到的另一种C技术是成功返回0并且出错,返回-1并设置errno
以指示错误。
你提出的技巧各有利弊,所以决定哪一个是“最好的”将始终(至少部分)主观。但是,我可以毫无保留地说出这一点:最好的技术是在整个程序中保持一致的技术。在程序的不同部分使用不同类型的错误报告代码很快就会成为维护和调试的噩梦。
答案 5 :(得分:1)
许多标准库函数的较新安全版本使用第二种方法 - 显式返回状态。
此处没有例外。例外情况是代码可能无法处理的特殊情况 - 您不会为strcmp()中不匹配的字符串引发异常
答案 6 :(得分:1)
并非总是可行,但无论您使用哪种错误报告方法,最佳做法是尽可能设计一个功能,使其不会出现故障情况,并且在不可能的情况下,尽量减少可能的错误情况。一些例子:
您可以设计程序,以便调用者打开文件并传递FILE *
或文件描述符,而不是通过许多函数调用向下传递文件名。这消除了对“无法打开文件”的检查,并在每一步都将其报告给调用者。
如果有一种廉价的方法来检查(或找到一个上限)一个函数将需要为它构建和返回的数据结构分配的内存量,提供一个函数来返回该数量并具有调用者分配内存。在某些情况下,这可能允许调用者简单地使用堆栈,从而大大减少内存碎片并避免malloc
中的锁定。
当某个函数正在执行您的实现可能需要大工作空间的任务时,请询问是否存在具有O(1)
空间要求的备用(可能更慢)算法。如果性能不重要,只需使用O(1)
空间算法即可。否则,如果分配失败,请实施回退案例以使用它。
这些只是一些想法,但是全部采用相同的原则可以真正减少您必须处理的错误条件的数量,并通过多个调用级别传播。
答案 7 :(得分:1)
您错过了一种方法:返回失败指示并要求额外调用以获取错误的详细信息。
对此有很多话要说。
示例:
int count;
if (!TryParse("12x3", &count))
DisplayError(GetLastError());
修改的
这个答案产生了相当多的争议和贬低。坦率地说,我完全不相信不同的论点。分离是否调用是否成功为什么失败已被证明是非常好的主意。将这两种力量组合成以下模式:
HKEY key;
long errcode = RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key);
if (errcode != ERROR_SUCCESS)
return DisplayError(errcode);
与此对比:
HKEY key;
if (!RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key))
return DisplayError(GetLastError());
(GetLastError版本与Windows API 通常的工作原理一致,但由于注册表API不遵循该标准,直接返回代码的版本实际上是如何工作的。)< / p>
在任何情况下,我都会建议错误返回模式让人很容易忘记为什么函数失败,导致代码如下:
HKEY key;
if (RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key) != ERROR_SUCCESS)
return DisplayGenericError();
修改的
看看R.的请求,我发现了一个实际可以满足的场景。
对于通用C风格的API,例如我在我的示例中使用的Windows SDK函数,没有非全局上下文来放置错误代码。相反,我们没有使用的好选择可以在失败后检查的全局TLV。
但是,如果我们扩展主题以在类中包含方法,情况就不同了。如果变量reg
是RegistryKey
类的一个实例,则调用reg.Open
返回false
,要求我们调用{{1},这是完全合理的检索细节。
我相信这满足了R.的请求,即错误代码是上下文的一部分,因为实例提供了上下文。如果我们在reg.ErrorCode
上调用静态RegistryKey
方法而不是Open
实例,那么在失败时检索错误代码同样必须是静态的,这意味着它将具有成为TLV,虽然不是完全全球性的。与实例相反,该类将是上下文。
在这两种情况下,面向对象都提供了一个用于存储错误代码的自然上下文。话虽如此,如果没有自然上下文,我仍然会坚持全局,而不是试图强制调用者传入输出参数或其他人工上下文,或直接返回错误代码。
答案 8 :(得分:1)
两者之间的性能差异应该不大(如果有的话)。选择取决于具体用途。如果没有合适的无效值,则无法使用第一个。
如果使用C ++,除了这两个之外还有更多的可能性,包括异常并使用boost :: optional作为返回值。
答案 9 :(得分:1)
对于C ++,我赞成一个模板化的解决方案,它可以防止出现参数和在组合答案/返回代码中出现“幻数”的琐事。我在answering another question期间对此进行了阐述。看一看。
对于C来说,我觉得这些笨拙的参数不如那些狡猾的“神奇数字”更具攻击性。
答案 10 :(得分:0)
如果您有引用和bool类型,则必须使用C ++。在这种情况下,抛出异常。这就是他们的目的。对于一般的桌面环境,没有理由使用错误代码。我在一些环境中看到了反对异常的论据,比如狡猾的语言/进程互操作或紧密的嵌入式环境。假设这两者都不总是抛出异常。
答案 11 :(得分:0)
好吧,第一个将在C和C ++中编译,所以要做便携式代码就可以了。 第二个,虽然它更“人类可读”,但你永远不知道程序返回哪个值,在第一个案例中指定它会给你更多的控制权,这就是我的想法。
答案 12 :(得分:0)
我认为一个好的编译器会以相同的速度生成几乎相同的代码。这是个人偏好。我会继续。
答案 13 :(得分:0)
我更喜欢使用返回码来表示发生的错误类型。这有助于API的调用者采取适当的错误处理步骤。
考虑GLIB API,它通常会返回错误代码和错误消息以及布尔返回值。
因此,当您获得函数调用的否定返回时,可以从GError变量中检查上下文。
您指定的第二种方法失败无法帮助调用者采取正确的操作。当您的文档非常清楚时,它的情况就不同了。但在其他情况下,找到如何使用API调用将会很头疼。
答案 14 :(得分:0)
对于“try”函数,其中一些“正常”类型的失败是合理预期的,如何接受默认返回值或指向函数的指针,该函数接受与失败相关的某些参数并返回这样的值预期的类型?
答案 15 :(得分:0)
我认为没有正确答案。这取决于您的需求,整体应用程序设计等。我个人使用第一种方法。
答案 16 :(得分:-4)
除了正确的方式之外,您更喜欢这两种愚蠢的方式中的哪一种?
当我使用C ++并且需要抛出错误时,我更喜欢使用异常,而且通常,当我不想强制所有调用函数检测和处理错误时。当只有一个可能的错误条件时,我更喜欢使用愚蠢的特殊值,并且该条件意味着调用者无法继续进行,并且每个可以想到的调用者都能够处理它......这很少见。我更喜欢在修改旧代码时使用愚蠢的参数,出于某种原因,我可以更改参数的数量,但不能更改返回类型或标识特殊值或抛出异常,这是迄今为止从未有过的。
是否有其他参数 第二种方法值得注意 性能开销?
是的!其他参数会导致您的电脑减速至少0纳秒。最好在该参数上使用“无开销”关键字。它是GCC扩展__attribute__((no-overhead))
,所以是YMMV。