我们对抛出C ++中找不到的异常有何看法?

时间:2020-03-06 14:49:30  来源:igfitidea点击:

我知道大多数人认为这是一种不好的做法,但是当我们尝试使类的公共接口只能与引用一起使用,将指针保留在内部且仅在必要时,我认为无法返回任何信息来表明我们正在寻找的值在容器中不存在。

class list {
    public:
        value &get(type key);
};

假设我们不想在类的公共接口中看到危险的指针,那么在这种情况下如何返回未找到的对象并引发异常?

处理方法是什么?我们是否返回一个空值并检查它的空状态?我实际上使用了throw方法,但我介绍了一种检查方法:

class list {
   public:
      bool exists(type key);
      value &get(type key);
};

因此,当我忘记首先检查该值是否存在时,我会收到一个异常,这确实是一个异常。

你会怎么做?

解决方案

有趣的问题。在C ++中,专门使用引用是一个问题,我猜在Java中,引用更灵活,可以为null。我不记得强制使用空引用是合法的C ++:

MyType *pObj = nullptr;
return *pObj

但是我认为这很危险。同样,在Java中,我会抛出一个异常,因为这在那里很常见,但是我很少看到在C ++中如此自由地使用异常。
如果我要为可重用的C ++组件制作特殊的API并必须返回引用,我想我会走异常路线。
我真正的偏好是让API返回一个指针。我认为指针是C ++不可或者缺的一部分。

我更喜欢在这种情况下执行的操作是抛出" get",而对于那些性能或者故障很常见的情况,可以按照" bool tryGet(type key,value ** pp)"的方式使用" tryGet"函数合同规定,如果返回true,则* pp ==指向某个对象的有效指针,否则* pp为null。

STL通过使用迭代器来处理这种情况。例如,std :: map类具有类似的功能:

iterator find( const key_type& key );

如果找不到该键,则返回" end()"。我们可能要使用此迭代器方法,或者对返回值使用某种包装器。

如何返回一个shared_ptr作为结果。如果找不到该项目,则可以为null。它的工作方式类似于指针,但是会为我们释放对象。

在这种情况下,请勿使用异常。即使没有引发任何异常,C ++对于此类异常也有不小的性能开销,而且它使代码推理更加困难(参见异常安全性)。

C ++的最佳实践是以下两种方法之一。两者都在STL中使用:

  • 正如Martin所指出的,返回一个迭代器。实际上,对于一个简单的指针,迭代器很可能是typedef,没有什么反对的。实际上,由于这与STL一致,因此我们甚至可以争辩说,这种方法优于返回引用。
  • 返回一个std :: pair <bool,yourvalue>。但是,这使得无法修改该值,因为调用了对的copycon,但不适用于被提名人成员。

/编辑:

这个答案引起了很大的争议,从评论中可以看出来,而从获得的许多赞成票中却看不到这一点。我发现这很令人惊讶。

从来没有将此答案作为最终参考。马丁已经给出了正确的答案:观念在这种情况下反映的行为相当差。使用其他信令机制在语义上比异常更有意义。

美好的。我完全赞同这种观点。无需再次提及。取而代之的是,我想给答案一个额外的方面。尽管小幅提高速度绝不是任何决策的首要理由,但它们可以提供进一步的论据,在某些(少数)情况下,它们甚至可能至关重要。

实际上,我已经提到了两个方面:性能和异常安全。我相信后者是没有争议的。尽管很难给出强大的异常保证(最强的当然是未抛出异常),但我相信这是必不可少的:任何保证不会抛出异常的代码都使整个程序更容易推理。许多C ++专家都强调这一点(例如,Effective C ++项目29中的Scott Meyers)。

关于速度。马丁·约克(Martin York)指出,这不再适用于现代编译器。我谨不同意。 C ++语言使环境有必要在运行时跟踪在发生异常的情况下可能会解开的代码路径。现在,这些开销实际上并没有那么大(并且可以很容易地验证这一点)。在我上面的文字中,平凡的举动可能太强了。

但是,我发现区分诸如C ++之类的语言与诸如C#之类的许多现代托管语言之间的区别非常重要。只要不抛出异常,后者就不会有任何额外的开销,因为无论如何,展开堆栈所需的信息都会保留。总的来说,请支持我的选择。

exist()的问题在于,我们将最终搜索两次确实存在的事物(首先检查它是否在其中,然后再次找到它)。这是低效的,特别是如果容器(如"列表"的名称所暗示的)是其中搜索为O(n)的容器时,尤其如此。

当然,我们可以进行一些内部缓存来避免双重搜索,但是这样会使实现变得更混乱,类将变得不那么通用(因为我们已经针对特定情况进行了优化),并且它可能不会是异常安全的或者线程化的-安全的。

STL迭代器?

在我之前提出的"迭代器"想法很有趣,但是迭代器的真正目的是在容器中导航。不是简单的访问器。

如果访问者是众多访问者之一,那么迭代器就是理想之选,因为我们将能够使用它们在容器中移动。但是如果访问器是一个简单的获取器,能够返回值或者没有值的事实,那么迭代器可能只是一个美化的指针...

这导致我们...

智能指针?

智能指针的目的是简化指针所有权。使用共享指针,我们将获得资源(内存),该资源将被共享,但会增加开销(共享指针需要分配一个整数作为参考计数器...)。

我们必须选择:Value已经在共享指针中,然后,我们可以返回此共享指针(或者弱指针)。或者值在原始指针内。然后,我们可以返回行指针。如果资源尚未位于共享指针中,则我们不希望返回共享指针:当共享指针超出范围并删除Value而不告诉我们时,将会发生很多有趣的事情。

:-p

指针?

如果接口清楚其资源的所有权,并且实际上返回的值可以为NULL,那么我们可以返回一个简单的原始指针。如果代码用户很笨,可以忽略对象的接口协定,或者用指针进行算术运算或者其他操作,那么他/她将很笨拙,可以破坏我们选择返回值的任何其他方式,因此不要理会那些精神错乱的人...

不确定值

除非Value类型确实已经具有某种"未定义"的值,并且用户知道这一点并愿意接受处理,否则它是一种可能的解决方案,类似于指针或者迭代器解决方案。

但是不要因为我们遇到的问题而在Value类中添加"未定义"的值:我们最终将"引用与指针"的争执提高到另一个层次。代码用户希望我们提供给他们的对象可以,或者不存在。必须测试该对象仍然有效的所有其他代码行,这是一个痛苦,并且会因过错而使用户代码变得无用。

例外情况

通常,例外并不像某些人希望的那样代价高昂。但是对于一个简单的访问器来说,如果经常使用访问器,那么代价可能并不小。

例如,STL std :: vector通过索引有两个访问其值的访问器:

T & std::vector::operator[]( /* index */ )

和:

T & std::vector::at( /* index */ )

不同之处在于[]是非抛出的。因此,如果访问向量范围之外的内容,我们将是一个人,可能会导致内存损坏,并且迟早会崩溃。因此,我们应该确保使用它验证了代码。

另一方面," at"在抛出。这意味着,如果我们访问向量范围之外的内容,则会得到一个干净的异常。如果要将错误处理委托给另一个代码,则此方法更好。

访问循环或者类似内容中的值时,我会单独使用[]。当我感觉异常是返回当前代码(或者调用代码)的好方法时,我使用了at,这是事实出了问题。

所以呢?

对于情况,我们必须选择:

如果我们确实需要闪电般的访问权限,那么抛出访问器可能是个问题。但这意味着我们已经在代码上使用了探查器来确定这是一个瓶颈,不是吗?

;-)

如果我们知道没有值的情况经常发生,并且/或者我们希望客户端将可能的null /无效/任何语义指针传播到所访问的值,则返回一个指针(如果值位于简单指针内部)或者弱/共享指针(如果值由共享指针拥有)。

但是,如果我们认为客户端不会传播此"空"值,或者他们不应在其代码中传播NULL指针(或者智能指针),则请使用受异常保护的引用。添加一个返回布尔值的" hasValue"方法,并在用户尝试获取该值的情况下添加一个throw,即使没有该值也是如此。

最后但并非最不重要的一点,请考虑对象用户将使用的代码:

// If you want your user to have this kind of code, then choose either
// pointer or smart pointer solution
void doSomething(MyClass & p_oMyClass)
{
   MyValue * pValue = p_oMyClass.getValue() ;

   if(pValue != NULL)
   {
      // Etc.
   }
}

MyValue * doSomethingElseAndReturnValue(MyClass & p_oMyClass)
{
   MyValue * pValue = p_oMyClass.getValue() ;

   if(pValue != NULL)
   {
      // Etc.
   }

   return pValue ;
}

// ==========================================================

// If you want your user to have this kind of code, then choose the
// throwing reference solution
void doSomething(MyClass & p_oMyClass)
{
   if(p_oMyClass.hasValue())
   {
      MyValue & oValue = p_oMyClass.getValue() ;
   }
}

因此,如果主要问题是在上述两个用户代码之间进行选择,则问题不在于性能,而在于"代码人体工程学"。因此,由于潜在的性能问题,不应将异常解决方案放在一边。

:-)

The "iterator" idea proposed before me is interesting, but the real point of iterators is navigation through a container. Not as an simple accessor.

我同意paercebal的观点,迭代器是要迭代的。我不喜欢STL的方式。但是访问器的想法似乎更具吸引力。那我们需要什么?像class这样的容器,感觉像是一个用于测试的布尔值,但其行为类似于原始的返回类型。对于演员来说这是可行的。

template <T> class Accessor {
    public:
        Accessor(): _value(NULL) 
        {}

        Accessor(T &value): _value(&value)
        {}

        operator T &() const
        {
            if (!_value)
               throw Exception("that is a problem and you made a mistake somewhere.");
            else
               return *_value;
        }

        operator bool () const
        {
            return _value != NULL;
        }

    private:
        T *_value;
};

现在,有任何可预见的问题吗?用法示例:

Accessor <type> value = list.get(key);

if (value) {
   type &v = value;

   v.doSomething();
}

正确答案(根据Alexandrescu)是:

首先,一定要使用访问器,但要安全得多,不要发明轮子:

boost::optional<X> get_X_if_possible();

然后创建一个"强制"助手:

template <class T, class E>
T& enforce(boost::optional<T>& opt, E e = std::runtime_error("enforce failed"))
{
    if(!opt)
    {
        throw e;
    }

    return *opt;
}

// and an overload for T const &

这样,根据缺少该值的含义,我们可以明确检查:

if(boost::optional<X> maybe_x = get_X_if_possible())
{
    X& x = *maybe_x;

    // use x
}
else
{
    oops("Hey, we got no x again!");
}

或者隐含地:

X& x = enforce(get_X_if_possible());

// use x

当我们关注效率或者要在发生故障的地方正确处理故障时,可以使用第一种方法。第二种方法适用于所有其他情况。

@aradtke,我们说过。

I agree with paercebal, an iterator is
  to iterate. I don't like the way STL
  does. But the idea of an accessor
  seems more appealing. So what we need?
  A container like class that feels like
  a boolean for testing but behaves like
  the original return type. That would
  be feasible with cast operators. [..] Now,
  any foreseeable problem?

首先,我们不希望操作员笨蛋。有关更多信息,请参见"安全布尔"成语。但是关于你的问题...

这就是问题所在,用户现在需要在情况下明确显示演员表。类似指针的代理(例如迭代器,ref-counted-ptrs和原始指针)具有简洁的" get"语法。如果调用者必须使用额外的代码来调用它,则提供转换操作符不是很有用。

从引用(例如示例)开始,最简洁的编写方式是:

// 'reference' style, check before use
if (Accessor<type> value = list.get(key)) {
   type &v = value;
   v.doSomething();
}
// or
if (Accessor<type> value = list.get(key)) {
   static_cast<type&>(value).doSomething();
}

没关系,请不要误解我的意思,但是它比必须的要冗长得多。现在考虑是否由于某种原因而知道list.get成功。然后:

// 'reference' style, skip check 
type &v = list.get(key);
v.doSomething();
// or
static_cast<type&>(list.get(key)).doSomething();

现在让我们回到迭代器/指针的行为:

// 'pointer' style, check before use
if (Accessor<type> value = list.get(key)) {
   value->doSomething();
}

// 'pointer' style, skip check 
list.get(key)->doSomething();

两者都很好,但是指针/迭代器的语法略短一些。我们可以为"引用"样式赋予成员函数" get()" ...,但这已经是operator *()和operator->()的目的。

"指针"样式的访问器现在具有运算符" unspecified bool",运算符*和operator->。

猜猜是什么...原始指针满足这些要求,因此对于原型来说,list.get()返回T *而不是Accessor。然后,当列表的设计稳定时,我们可以返回并编写访问器,它是一种类似指针的代理类型。

(我意识到这并不总是正确的答案,而且我的语气有些强烈,但是在决定其他更复杂的选择之前,我们应该考虑这个问题):

那么,返回指针到底有什么问题呢?

我在SQL中已经多次见过这种情况,人们会竭尽所能,从不处理NULL列,就像他们有传染性的死亡之类的东西一样。相反,他们巧妙地提出了"空白"或者"不存在"的人工值,例如-1、9999或者什至是" @ X-EMPTY-X @"。

我的回答是:该语言已经具有"不存在"的构造;继续,不要害怕使用它。