【问题标题】:c++ function that can't return a meaningful value under some conditions在某些条件下无法返回有意义值的 c++ 函数
【发布时间】:2012-10-26 14:13:37
【问题描述】:

我有一个以对象类型作为返回值类型的成员函数:

MyObject myfunction(parameters) {
    if (some condition) { 
        return MyObject(parameters);
    } else { 
        ... no valid object can be created ... 
    } 
}

在某些条件下(在函数体中检查)无法创建和返回 MyObject 类型的对象。

作为一个偶尔的 c++ 程序员,我可以自发地想出三个解决方案:

  1. 将返回值类型更改为 * MyObject 并在无法创建有效对象时返回 nullptr (C++11),然后在调用代码中检查与 nullptr 是否相等。
  2. 如果无法创建对象并在调用代码中捕获该对象,则引发异常。
  3. 使用我定义为无效的一些值创建一个对象,并在使用返回的对象之前检查这些值。

处理这种情况的标准方法和性能方面的最佳解决方案是什么? ...或者一些我看不到的明显解决方法...

最先进的 C++11 解决方案将是完美的 :-)

到目前为止我的想法:
解决方案 1 看起来不错,但仅适用于 C++11,我必须在堆上创建返回的对象才能将其传递给主程序(将对象本身返回给调用函数,从而将其保留在对于小物体,堆栈可能更快?)。
解决方案 2 可能会更慢,并导致主程序中的编码冗长。
解决方案 3 可能是最慢的(徒劳地创建了一个对象),并且在主程序中检查不是很方便。

对于我的代码,没有有效的返回对象是默认情况而不是异常,并且创建的对象相当小,但考虑不同情况的一般考虑对于其他读者的应用程序肯定有用...

非常感谢大家的帮助:-)

【问题讨论】:

  • 我不知道myfunction 是做什么的,我也不知道MyObject 是什么。我怎样才能给你一个最好的解决方案?
  • 通常我会说 nullptr,但这真的取决于你的项目。
  • 如果你真的需要返回实例而不是这个函数中的指针抛出异常将是正确的方法恕我直言
  • myFunction 的用途是什么?它是否存在只是为了让您可以在失败的情况下做一些不同的事情?上述所有三个选项的替代方案可能是在某些情况下简单地在 MyObject 的构造函数中抛出异常。
  • @datamole 问题在于这两个目标是矛盾的,至少从某人回答的角度来看:尽可能简单是一个非常具体的例子;尽可能笼统地不会要求最佳解决方案,而是要求做出决定的主要驱动动机是什么。事实上,这不是一个简单的问题(需要考虑的相关变量太多),也不是一个普遍的问题(它要求一个最佳解决方案,因此它不可能适用于许多情况)

标签: c++ function pointers null


【解决方案1】:

在通常情况下,返回 Boost.Optional 有效:

boost::optional<MyObject> myfunction(parameters) {
    if (some condition) { 
        return MyObject(parameters);
    } else { 
        return boost::none;
    } 
}

在通话现场:

auto ret = myfunction(...);
if(ret)
  // use '*ret'  or 'ret.get()'

但正如 R. Martinho 所提到的,这种解决方案存在一些缺点(即,仅移动类型不起作用,因为 Boost.Optional 尚未更新以支持移动语义)。

【讨论】:

  • 如果你想避免使用 boost 库也不起作用。但是,编写等效类型相当容易。我认为这是最好的主意,如果:(a)对象很小,所以在堆上创建是次优的; (b) 无法创建对象很常见。如果创建对象失败的情况很少见,那么抛出异常更有意义。
  • @PeterRuderman:大小与这里的任何东西有什么关系?我真的不建议在任何情况下在堆上创建东西,除非你冒着大对象堆栈溢出的风险。此外,真的没有理由避免使用 Boost。
  • @Xeo:如果“MyObject”很小,那么在堆上创建它会非常浪费(特别是如果他必须创建大量这些对象)。另一方面,如果必须在之后复制和/或移动对象,则在堆栈上创建大对象也可能是无效的。
  • @Peter:复制省略应该处理 99% 的时间。
  • @Mooing:好吧,也许我应该说“没有理性理由避免使用 Boost”。 ://
【解决方案2】:

根据具体情况,您建议的所有 3 个解决方案都是有效且常见的。

如果无法创建对象是可能导致调用函数不得不中止、备份并重试或采取其他极端措施的错误条件,则抛出异常。

如果无法创建对象是例行事件,并且您希望调用者检查对象是否已创建并在任何一种情况下都正常进行,则返回 null 是一个很好的解决方案。

如果可以创建一个合理的虚拟或空白对象,那是一个很好的解决方案。但这非常罕见。仅当调用者将实际处理虚拟对象时,您才应该这样做。

如果你返回一个空指针,然后你发现你调用这个函数的每个地方都在写

MyObject* myobject=myfunction(whatever);
if (myobject==null) throw new PanicException;

那你还不如直接在函数里面抛出异常。

更糟糕的是,如果你正在写作:

MyObject* myobject=myfunction(whatever);
if (myobject!=null)
{
  ... process it ...
}
else
{
   ... display error message ...
}

那么您只是在使用 IF 语句模拟异常处理。使用真正的异常。

另一方面,如果你抛出一个异常,然后你发现你经常写:

MyObject* myobject;
try
{
  myobject=myfunction(whatever);
}
catch (PanicException pe)
{
  myobject=null;
}

那么,你最好只返回空值。

我偶尔会创建虚拟对象。最常见的情况是当一个函数返回一个集合(如数组或链表)时,如果我找不到要放入集合的数据,则返回一个包含零元素的集合。然后调用者循环遍历集合中的元素,如果没有,那很好。我遇到过一些情况,其中我返回了一个带有零长度字符串的对象,用于名称或客户 ID 或其他任何内容。但总的来说,如果您只是返回一个虚拟对象,以便调用者可以测试并说,哦,这是一个虚拟对象,然后将其丢弃,我认为您最好返回 null。

顺便说一句,当你说你只能在 C++11 中返回一个空指针时,我不确定你的意思。传递空值的能力可以追溯到我见过的最早的 C++ 版本。

【讨论】:

  • 'nullptr' 在 C++11 中不存在,但 'NULL' 存在。
【解决方案3】:

由于您的问题是笼统的,我也将笼统地回答。

如果你有一个函数,它的工作是创建和返回一个对象,那么 that 就是它的工作。

现在,如果您想以这样一种方式设计此函数,以便在满足构建对象所需的某些条件时不返回对象,您实际上已经更改了此函数的语义.现在,它只承担一项职责,而是承担三项职责:

  1. 确定是否存在构建对象的正确条件
  2. 如果是,则构造并返回对象
  3. 如果否,则不返回任何内容,或者返回一些指示未创建的条件值

Single Responsibility Principle”表明一般来说,好的设计要求一个函数(或类或你有什么)应该有一项工作要做。在这里,你的函数有三个。

我建议您建议的方法一般都不是最好的。相反,我会选择:

4:实现一个单独的函数来确定是否有资格 构造对象。如果该函数返回 true,则调用 myFunction 构造对象并返回它。

【讨论】:

  • 我衷心地...不同意。您在这里对 SRP 的使用过于严格:您当然可以预先验证参数是否处于正确状态,但是对于 *防御性编程 要求该函数也进行该检查,以防万一被遗忘了,你又回到了第一格 => 如果参数不正确,函数应该如何表现?
  • @MatthieuM.:当然,你说得对,代码应该是防御性的。但是,如果调用该函数来创建一个对象但它不能,我会认为这是一种特殊情况。在此之后,在这种情况下,我会抛出异常。最好来自相关对象本身的构造函数。
  • 但是你的构造函数正在做两件事:决定它是否可以构建对象,以及构建对象。你又回到了你开始的地方。我一般同意单一职责的想法,但如果你试图将“过程参数”与“验证参数”分开,我认为你的想法太过分了。接下来是什么?一个函数不能既构建一个对象又返回它,因为那将是两件事?在实践中,当我们试图做某事时,我们常常最自然地发现我们做不到。比如,用某个键读取记录...哎呀,找不到记录...
  • ...计算订单总数...哎呀,超出订单限制。等等。如果我们尝试为这些事情编写预检查,我们最终将编写大量代码两次:一次进行计算或查找或其他任何事情,以便我们可以测试它是否可能,然后再次实际做这项工作。我宁愿每件事都做一次。
【解决方案4】:

除非阻止创建对象的条件是异常,否则应使用第一种解决方案。否则返回 NULL 指针是一个完全有效的解决方案……即使在 C++11 中也是如此。

“指针”当然应该使用 RAII 容器(例如 std::unique_ptr)进行包装。真的应该是您编写代码的常见做法。

如果你问我,第三种解决方案完全是浪费资源。您必须创建一个无效(无用)的对象,并将其复制为返回值......只是为了将其丢弃。

【讨论】:

  • 返回指针的缺点是所有权不再清晰,也不再由编译器强制执行。 即使在 C++03 中.
  • @R.MartinhoFernandes 好的,我应该指定一个 std::unique_ptr,而不是原始指针。
  • 请注意,按值返回通常不会复制对象,因为 RVO:返回值优化。因此,您对第三种解决方案的反对只是部分有效:如果对象的创建成本低廉,则不会浪费任何资源。对第 3 种解决方案的真正反对意见是语义之一:具有“空”状态的对象可能更难操作,因为类不变量更松散。
猜你喜欢
  • 1970-01-01
  • 2019-12-03
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
  • 2017-01-15
  • 2020-02-05
  • 2019-03-16
  • 2020-06-14
相关资源
最近更新 更多