12

我知道大多数人认为这是一种不好的做法,但是当您试图使您的类公共接口仅与引用一起使用时,将指针保留在内部并且仅在必要时,我认为没有办法返回告诉您正在寻找的值的东西容器中不存在。

班级名单{
    上市:
        值 &get(键入键);
};

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

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

班级名单{
   上市:
      布尔存在(类型键);
      值 &get(键入键);
};

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

你会怎么做?

4

11 回答 11

16

The STL deals with this situation by using iterators. For example, the std::map class has a similar function:

iterator find( const key_type& key );

If the key isn't found, it returns 'end()'. You may want to use this iterator approach, or to use some sort of wrapper for your return value.

于 2008-09-27T15:12:10.043 回答
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

当您关心效率或想要在故障发生的地方处理故障时,您会使用第一种方法。第二种方式适用于所有其他情况。

于 2008-09-27T21:16:01.793 回答
5

Don't use an exception in such a case. C++ has a nontrivial performance overhead for such exceptions, even if no exception is thrown, and it additially makes reasoning about the code much harder (cf. exception safety).

Best-practice in C++ is one of the two following ways. Both get used in the STL:

  • As Martin pointed out, return an iterator. Actually, your iterator can well be a typedef for a simple pointer, there's nothing speaking against it; in fact, since this is consistent with the STL, you could even argue that this way is superior to returning a reference.
  • Return a std::pair<bool, yourvalue>. This makes it impossible to modify the value, though, since a copycon of the pair is called which doesn't work with referende members.

/EDIT:

This answer has spawned quite some controversy, visible from the comments and not so visible from the many downvotes it got. I've found this rather surprising.

This answer was never meant as the ultimate point of reference. The “correct” answer had already been given by Martin: execeptions reflect the behaviour in this case rather poorly. It's semantically more meaningful to use some other signalling mechanism than exceptions.

Fine. I completely endorse this view. No need to mention it once again. Instead, I wanted to give an additional facet to the answers. While minor speed boosts should never be the first rationale for any decision-making, they can provide further arguments and in some (few) cases, they may even be crucial.

Actually, I've mentioned two facets: performance and exception safety. I believe the latter to be rather uncontroversial. While it's extremely hard to give strong exceptions guarantees (the strongest, of course, being “nothrow”), I believe it's essential: any code that is guaranteed to not throw exceptions makes the whole program easier to reason about. Many C++ experts emphasize this (e.g. Scott Meyers in item 29 of “Effective C++”).

About speed. Martin York has pointed out that this no longer applies in modern compilers. I respectfully disagree. The C++ language makes it necessary for the environment to keep track, at runtime, of code paths that may be unwound in the case of an exception. Now, this overhead isn't really all that big (and it's quite easy to verify this). “nontrivial” in my above text may have been too strong.

However, I find it important to draw the distinction between languages like C++ and many modern, “managed” languages like C#. The latter has no additional overhead as long as no exception is thrown because the information necessary to unwind the stack is kept anyway. By and large, stand by my choice of words.

于 2008-09-27T15:21:04.377 回答
5

The problem with exists() is that you'll end up searching twice for things that do exist (first check if it's in there, then find it again). This is inefficient, particularly if (as its name of "list" suggests) your container is one where searching is O(n).

Sure, you could do some internal caching to avoid the double search, but then your implementation gets messier, your class becomes less general (since you've optimised for a particular case), and it probably won't be exception-safe or thread-safe.

于 2008-09-27T15:43:59.577 回答
2

STL 迭代器?

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

如果您的访问器是众多访问器之一,那么迭代器就是您要走的路,因为您将能够使用它们在容器中移动。但是,如果您的访问器是一个简单的 getter,能够返回value或没有 value的事实,那么您的迭代器可能只是一个美化的指针......

这导致我们...

智能指针?

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

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

:-p

指针?

如果您的接口清楚地知道其资源的所有权,并且返回的值可以是 NULL,那么您可以返回一个简单的原始指针。如果您的代码的用户足够愚蠢,可以忽略对象的接口契约,或者用您的指针进行算术运算或其他任何操作,那么他/她将足够愚蠢以破坏您选择返回值的任何其他方式,所以智障者不要打扰...

未定义值

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

但是不要因为您提出的问题而在您的 Value 类中添加“未定义”值:您最终会将“引用与指针”之战提升到另一个疯狂程度。代码用户希望您提供给他们的对象要么正常,要么不存在。必须测试该对象仍然有效的所有其他代码行是一种痛苦,并且会因您的错而使用户代码变得无用地复杂化。

例外

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

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

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

和:

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

不同之处[]在于是非投掷。因此,如果您在向量范围之外进行访问,则您只能靠自己,可能会冒着内存损坏的风险,并且迟早会崩溃。所以,你真的应该确保你使用它验证了代码。

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

[]当我访问循环内的值或类似的东西时,我会使用personnaly 。当我感觉到异常时,我使用at是返回当前代码(或调用代码)出错事实的好方法。

所以呢?

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

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

;-)

如果您知道没有值可能经常发生,并且/或者您希望您的客户端将可能的空/无效/任何语义指针传播到访问的值,然后返回一个指针(如果您的值在一个简单的指针内)或弱/共享指针(如果您的值由共享指针拥有)。

但是,如果您认为客户端不会传播此“空”值,或者他们不应该在其代码中传播 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() ;
   }
}

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

:-)

于 2008-09-27T19:02:06.827 回答
2

访问者?

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

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

模板 <T> 类访问器 {
    上市:
        访问器():_value(NULL)
        {}

        存取器(T &value): _value(&value)
        {}

        运算符 T &() 常量
        {
            如果(!_值)
               throw Exception("这是个问题,你在某处犯了错误。");
            别的
               返回 *_value;
        }

        运算符 bool () 常量
        {
            返回_值!= NULL;
        }

    私人的:
        T *_值;
};

现在,有什么可预见的问题吗?一个示例用法:

访问器 <type> value = list.get(key);

如果(值){
   类型 &v = 值;

   v.doSomething();
}
于 2008-09-27T20:37:35.583 回答
1

How about returning a shared_ptr as the result. This can be null if the item wasn't found. It works like a pointer, but it will take care of releasing the object for you.

于 2008-09-27T15:19:16.797 回答
1

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

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

我在 SQL 中多次看到过这种情况,人们会竭尽全力从不处理 NULL 列,就像他们有一些传染性的死亡或其他东西一样。相反,他们巧妙地提出了一个“空白”或“不存在”的人工值,例如 -1、9999 甚至类似“@X-EMPTY-X@”。

我的回答:该语言已经有一个“不存在”的结构;去吧,不要害怕使用它。

于 2008-09-28T05:04:24.473 回答
0

在这种情况下我更喜欢做的是抛出“get”,对于那些性能很重要或失败很常见的情况,有一个“tryGet”函数,类似于“bool tryGet(type key,value **pp)” whoose合同是,如果返回 true,则 *pp == 指向某个对象的有效指针,否则 *pp 为空。

于 2008-09-27T15:02:26.620 回答
0

@aradtke,你说。

我同意 paercebal 的观点,迭代器就是迭代。我不喜欢 STL 的做法。但是访问器的想法似乎更有吸引力。那么我们需要什么?一个类似容器的类,感觉就像一个用于测试的布尔值,但行为类似于原始返回类型。这对于演员表操作员来说是可行的。[..] 现在,有什么可预见的问题吗?

首先,您不需要操作员布尔值。有关更多信息,请参阅安全布尔成语。但是关于你的问题...

这就是问题所在,用户现在需要在案例中显式转换。类指针代理(例如迭代器、引用计数的指针和原始指针)具有简洁的“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();

两者都非常好,但指针/迭代器语法只是有点短。你可以给'reference'风格一个成员函数'get()'......但这已经是operator*()和operator->()的用途。

“指针”样式的访问器现在具有运算符“未指定的布尔值”、运算符* 和运算符->。

猜猜看……原始指针满足这些要求,因此对于原型设计,list.get() 返回 T* 而不是 Accessor。然后等list的设计稳定了,就可以回来写Accessor,一种指针式的Proxy类型。

于 2008-09-27T21:33:48.910 回答
-6

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

MyType *pObj = nullptr;
return *pObj

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

于 2008-09-27T15:01:20.397 回答