代码之家  ›  专栏  ›  技术社区  ›  Edwin Jarvis

在C++中没有找到异常时,你会怎么想?

  •  12
  • Edwin Jarvis  · 技术社区  · 16 年前

    坏习惯

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

    假设您不希望在类的公共接口中看到危险的指针,那么在这种情况下如何返回notfound,抛出异常?

    价值

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

    因此,当我忘记先检查值是否存在时,我会得到一个异常,这实际上是一个错误 例外 .

    11 回复  |  直到 16 年前
        1
  •  16
  •   Martin Cote    16 年前

    STL通过使用迭代器来处理这种情况。例如,std::map类有一个类似的函数:

    iterator find( const key_type& key );
    

        2
  •  6
  •   Roman Odaisky    16 年前

    正确答案(根据亚历山德雷斯库的说法)是:

    Optional 执行

    首先,一定要使用存取器,但要安全,不要发明轮子:

    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
    

    当你关心效率的时候,或者当你想在失败发生的地方处理它的时候,你就使用第一种方法。第二种方法适用于所有其他情况。

        3
  •  5
  •   Sam Stokes    16 年前

    exists()的问题是,您将搜索两次确实存在的东西(首先检查它是否在那里,然后再次找到它)。这是低效的,特别是如果(正如它的名字“list”所暗示的)您的容器是一个搜索是O(n)的容器。

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

        4
  •  5
  •   Community Bayu Bramantya    4 年前

    在这种情况下使用异常。C++对这些异常具有非平凡的性能开销,

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

    • typedef
    • std::pair<bool, yourvalue> . 但是,这使得无法修改该值,因为 pair

    /编辑:

    这个答案引发了相当多的争议,从评论中可以看到,但从许多反对票中却看不到。我发现这相当令人惊讶。

    好的。我完全赞同这种观点。不用再提了。相反,我想给答案增加一个方面。虽然轻微的速度提升永远不应该是任何决策的首要理由,但它们可以提供进一步的论据,在某些(少数)情况下,它们甚至可能是至关重要的。

    然而,我发现区分C++语言和许多现代的、托管语言如C.*等很重要。后者有 额外的开销,只要没有抛出异常,因为释放堆栈所需的信息仍然保留。总的说来,请支持我的措辞。

        5
  •  2
  •   Community Bayu Bramantya    4 年前

    STL迭代器?

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

    如果访问器是众多访问器中的一个,那么迭代器就是一种选择,因为您可以使用它们在容器中移动。但是如果您的访问器是一个简单的getter,那么可以返回 价值 或者事实上 无价值 ,那么您的迭代器可能只是一个美化的指针。。。

    所以我们。。。

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

    您必须选择:要么您的值已经在一个共享指针中,然后,您可以返回这个共享指针(或弱指针)。或者您的值在原始指针中。然后可以返回行指针。 如果资源不在共享指针内,则不希望返回共享指针: 当你的共享指针超出范围,删除你的值而不告诉你时,一个有趣的世界将会发生。。。

    :-第

    指针?

    未定义值

    除非您的值类型真的已经有某种“未定义”的值,并且用户知道这一点,并且愿意处理它,否则这是一种可能的解决方案,类似于指针或迭代器解决方案。

    因为 关于你问的问题:你最终会把“参考与指针”的战争提升到另一个疯狂的水平。代码用户希望您提供给他们的对象要么正常,要么不存在。必须测试每一行代码这个对象仍然是有效的是一个痛苦,并将复杂化无用的用户代码,由您的错误。

    例外情况

    例外情况通常不像一些人希望的那样昂贵。但是对于一个简单的访问器来说,如果您的访问器经常被使用的话,那么它的成本就不小了。

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

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

    以及:

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

    [] 非投掷 . 因此,如果访问超出向量的范围,就只能靠自己了,可能会有内存损坏的风险,迟早会崩溃。所以,你应该确定你用它验证了代码。

    at 投掷 . 这意味着,如果您在向量的范围之外访问,那么您将得到一个干净的异常。如果您希望将错误的处理委托给另一个代码,则此方法更好。

    我经常用人 当我访问循环中的值或类似的东西时。我用 当我觉得异常是返回当前代码(或调用代码)的好方法时,就会发现出了问题。

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

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

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

    但是如果您认为客户机不会传播这个“null”值,或者他们不应该在代码中传播null指针(或智能指针),那么请使用受异常保护的引用。添加一个返回布尔值的“hasValue”方法,并在用户尝试获取值(即使没有)时添加一个抛出。

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

    :-)

        6
  •  2
  •   Edwin Jarvis    16 年前

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

    紫檀 ,迭代器是 迭代 . 我不喜欢STL的方式。但是访问器的想法似乎更有吸引力。我们需要什么?一个类似于容器的类,感觉像是用于测试的布尔值,但行为类似于原始返回类型。这对于cast操作符是可行的。

    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
  •   Scott Langham    16 年前

    作为结果返回一个共享的ptr如何。如果找不到项,则可以为空。它的工作原理类似于指针,但它将负责为您释放对象。

        8
  •  1
  •   Euro Micelli    16 年前

    应该 在决定其他更复杂的替代方案之前,请考虑这个问题:

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

    我在SQL中见过很多次这种情况,在SQL中,人们会尽最大努力永远不要处理空列,就像他们有传染病或其他什么。相反,他们巧妙地提出了一个“空白”或“不存在”的人工值,比如-19999,甚至类似于'@X-EMPTY-X@'。

    我的回答是:语言已经有了“不在那里”的结构;去吧,别害怕用它。

        9
  •  0
  •   Torbjörn Gyllebring    16 年前

        10
  •  0
  •   Aaron    16 年前

    我同意paercebal的观点,迭代器是 迭代。我不喜欢STL的方式 似乎更有吸引力。我们需要什么? 用于测试的布尔值,但其行为类似于 原始返回类型。那会的 对cast运算符是可行的。[..]现在,

    Safe Bool idiom 更多信息。但是关于你的问题。。。

    问题是,用户现在需要在案例中解释cast。指针式- proxies (例如迭代器、ref计数的ptr和原始指针)有一个简洁的“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();
    }
    

    这没关系,别误会我的意思,但这比必须的要详细得多。现在考虑一下 知道

    // '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();
    

    两者都很好,但指针/迭代器语法只是稍微短了一点。你可以给'reference'样式一个成员函数'get()'。。。但这已经是运算符*()和运算符->()是给你的。

    “pointer”样式的访问器现在具有运算符“unspecified bool”、运算符*和运算符->。

    Proxy 类型。

        11
  •  -6
  •   MidnightGun    16 年前

    MyType *pObj = nullptr;
    return *pObj
    

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