您如何看待在C ++中找不到的异常?

时间:2008-09-27 14:53:58

标签: c++ exception containers usagepatterns

我知道大多数人都认为这是一个糟糕的做法但是当你试图让你的类公共接口只使用引用时,保持指针在内部并且仅在必要时,我认为没有办法返回一些东西,告诉您容器中不存在您正在查找的值。

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

让我们认为您不希望在类的公共接口中看到危险指针,在这种情况下如何返回未找到的异常,抛出异常?

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

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

因此,当我忘记检查该值是否存在时,我得到一个异常,这实际上是一个异常

你会怎么做?

11 个答案:

答案 0 :(得分:16)

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

iterator find( const key_type& key );

如果找不到密钥,则返回'end()'。您可能希望使用此迭代器方法,或者使用某种包装器作为返回值。

答案 1 :(得分:6)

正确答案(根据Alexandrescu说):

Optional和强制执行

首先,使用Accessor,但是在没有发明轮子的情况下以更安全的方式使用:

boost::optional<X> get_X_if_possible();

然后创建一个enforce帮助器:

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

当您担心效率时,或者当您想要在发生故障时处理故障时,您可以使用第一种方式。第二种方式适用于所有其他情况。

答案 2 :(得分:5)

在这种情况下不要使用例外。 C ++对于此类异常具有非常重要的性能开销,即使没有抛出异常,它也会使代码的推理更加困难(参见异常安全性)。

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

  • 正如Martin指出的那样,返回一个迭代器。实际上,对于一个简单的指针,你的迭代器很可能是一个typedef,没有什么能反对它;事实上,由于这与STL一致,你甚至可以说这种方式优于返回参考。
  • 返回std::pair<bool, yourvalue>。但是,这使得无法修改该值,因为调用了pair的copycon,这对于参考成员不起作用。

/编辑:

这个答案引起了一些争议,从评论中可以看出,并且从它得到的许多贬值中看不到。我发现这很令人惊讶。

这个答案从未被视为最终的参考点。马丁已经给出了“正确”的答案:例外反映了这种情况下的行为相当糟糕。使用除异常之外的其他信令机制在语义上更有意义。

精细。我完全赞同这种观点。无需再次提及它。相反,我想为答案提供额外的方面。虽然轻微的速度提升永远不应该是任何决策的第一个理由,但它们可以提供进一步的论据,而在一些(少数)情况下,它们甚至可能是至关重要的。

实际上,我提到了两个方面:性能和异常安全。我相信后者是相当无可争议的。虽然提供强大的异常保证非常困难(当然,最强大的是“nothrow”),但我认为这是必不可少的:任何保证不会抛出异常的代码都会使整个程序更容易推理。许多C ++专家都强调这一点(例如Scott Meyers在“Effective C ++”第29项中)。

关于速度。 Martin York指出,这不再适用于现代编译器。我恭敬地不同意。 C ++语言使得环境必须在运行时跟踪在异常情况下可能解开的代码路径。现在,这个开销实际上并不是那么大(并且很容易验证这一点)。我上面的文字中“非常重要”可能过于强烈。

但是,我发现在C ++等语言和许多现代的“托管”语言(如C#)之间进行区分非常重要。只要没有抛出异常,后者就会有 no 额外的开销,因为无论如何都要保持展开堆栈所需的信息。总的来说,坚持我的选择。

答案 3 :(得分:5)

exists()的问题在于你最终会搜索两次存在的东西(首先检查它是否在那里,然后再找到它)。这是低效的,特别是如果(因为它的名称“列表”暗示)你的容器是搜索是O(n)的容器。

当然,你可以做一些内部缓存以避免双重搜索,但是你的实现变得更加混乱,你的类变得不那么通用了(因为你已针对特定情况进行了优化),并且它可能不会例外 - 安全或线程安全。

答案 4 :(得分:2)

STL迭代器?

在我之前提出的“迭代器”想法很有意思,但迭代器的真正意义在于通过容器导航。不是一个简单的访问者。

如果你的访问者是众多访问者之一,那么迭代器就是你要走的路,因为你可以使用它们来移动容器。但是如果你的访问者是一个简单的getter,能够返回值或没有值的事实,那么你的迭代器可能只是一个美化的指针... < / p>

这导致我们......

智能指针?

智能指针的重点是简化指针所有权。使用共享指针,您将获得一个将以开销为代价共享的资源(内存)(共享指针需要将整数分配为引用计数器......)。

您必须选择:您的Value已经在共享指针内,然后,您可以返回此共享指针(或弱指针)。或者您的值在原始指针内。然后你可以返回行指针。 如果你的资源不在共享指针中,你不想返回共享指针:当你的共享指针超出范围时会发生一些有趣的事情,删除你的值而不告诉你你...

: - P

指针?

如果你的界面清楚它的资源所有权,并且返回的值可以是NULL,那么你可以返回一个简单的原始指针。如果你的代码的用户是愚蠢的,忽略你的对象的接口契约,或者用你的指针玩算术或任何东西,那么他/她将愚蠢到打破你选择返回值的任何其他方式,所以不要为精神上的挑战而烦恼......

未定义值

除非您的Value类型确实已经存在某种“未定义”值,并且用户知道并且将接受处理它,否则它是一种可能的解决方案,类似于指针或迭代器解决方案。

但是不要在你的问题类中添加一个“未定义”值,因为:你最终会将“引用与指针”战争提升到另一个疯狂程度。代码用户希望您提供的对象可以是Ok,或者不存在。必须测试每个其他代码行,这个对象仍然有效是一种痛苦,并且会根据您的错误无用地复杂化用户代码。

例外

例外通常不像某些人希望的那样昂贵。但是对于一个简单的访问器,如果经常使用你的访问器,那么成本可能并非微不足道。

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

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

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

区别在于 [] 非投掷。因此,如果您在向量范围之外访问,那么您可以自己动手,可能会有内存损坏的风险,并且迟早会崩溃。所以,你应该确定你使用它来验证代码。

另一方面, at 投掷。这意味着如果您在向量范围之外访问,那么您将获得一个干净的异常。如果您想要委托另一个代码处理错误,这种方法会更好。

当我访问循环中的值或类似内容时,我使用personnaly [] 。我使用 at 当我觉得异常是返回当前代码(或调用代码)的好方法时出现问题。

那又怎样?

在您的情况下,您必须选择:

如果你真的需要快速访问,那么投掷访问器可能是个问题。但这意味着你已经在你的代码上使用了一个分析器来确定这是一个瓶颈,不是吗?

- )

如果你知道没有经常发生的值,和/或你希望你的客户端传播一个可能的null / invalid /任何语义指针到访问的值,那么返回一个指针(如果你的值在一个简单的内部)指针)或弱/共享指针(如果您的值由共享指针拥有)。

但是,如果您认为客户端不会传播此“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() ;
   }
}

因此,如果您的主要问题是在上述两个用户代码之间进行选择,那么问题不在于性能,而在于“代码人体工程学”。因此,由于潜在的性能问题,不应该搁置异常解决方案。

: - )

答案 5 :(得分:1)

如何返回shared_ptr作为结果。如果未找到该项,则此值可以为null。它就像一个指针,但它将负责为你释放对象。

答案 6 :(得分:1)

访问器?

  

在我之前提出的“迭代器”想法很有意思,但迭代器的真正意义在于通过容器导航。不是一个简单的访问者。

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

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();
}

答案 7 :(得分:1)

(我意识到这并不总是正确的答案,而且我的语气有点强,但你应该在考虑其他更复杂的替代方案之前考虑这个问题):

那么,返回一个指针有什么问题?

我在SQL中已经多次看到过这一点,人们会认真地从不处理NULL列,比如他们有一些传染性的死亡或其他东西。相反,他们巧妙地提出了一个“空白”或“不存在”的人工价值,如-1,9999,甚至是“@ X-EMPTY-X @”。

我的回答:语言已经有了“不存在”的结构;继续,不要害怕使用它。

答案 8 :(得分:0)

我喜欢在这种情况下做的是抛出“获取”而对于那些性能问题或失败很常见的情况,有一个“tryGet”功能,沿着“bool tryGet”(类型键,值** pp) )“whoose contract is if如果返回true则* pp ==指向某个对象的有效指针* pp为null。

答案 9 :(得分:0)

@aradtke,你说。

  

我同意paercebal,迭代器是   迭代。我不喜欢STL的方式   确实。但是访问者的想法   看起来更吸引人。那么我们需要什么呢?   一个类似容器的容器   一个用于测试的布尔值,但表现得像   原始的返回类型。那会   对于演员来说是可行的。 [..]现在,   任何可预见的问题?

首先,你不想要操作员布尔。有关详细信息,请参阅Safe Bool idiom。但关于你的问题......

这是问题所在,用户现在需要在案例中明确表达。类指针 - proxies(例如迭代器,ref-count-ptrs和原始指针)具有简洁的“get”语法。如果调用者必须使用额外的代码调用它,那么提供转换运算符并不是很有用。

从你的refence开始,例如,编写它的最简洁的方法:

// '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-&gt;()的用途。

'指针'样式Accessor现在具有运算符'未指定的bool',operator *和operator-&gt;。

猜猜是什么......原始指针满足这些要求,因此对于原型设计,list.get()返回T *而不是Accessor。然后,当列表的设计稳定时,您可以返回并编写一个类似指针的Proxy类型的Accessor。

答案 10 :(得分:-6)

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

MyType *pObj = nullptr;
return *pObj

但我认为这很危险。再次在Java中我会抛出一个异常,因为这很常见,但我很少看到在C ++中如此自由使用的异常。 如果我为可重用的C ++组件制作一个puclic API并且必须返回一个引用,我想我会去异常路由。 我真正的偏好是让API返回一个指针;我认为指针是C ++的一个组成部分。