简体   繁体   English

是否有任何理由不使用异常来测试std :: map中是否存在元素

[英]Is there any reason to not use exceptions to test if an element exists in a std::map

I've recently started using c++11 for a number of projects and have also started heavily using the stl containers which I'm relatively new to. 我最近开始在许多项目中使用c ++ 11,并且还开始大量使用我相对较新的stl容器。

I have a function I've recently written which does something similar to this: 我有一个我最近写的函数,它做了类似的事情:

CMyClass* CreateAndOrGetClass( int _iObjectId, std::map<int, CMyClass*>& _mapObjectData )
{
    CMyClass* pClassInstance{ nullptr };

    try
    {
        pClassInstance = _mapObjectData.at( _iObjectId);
    }
    catch ( ... )
    {
        pClassInstance = new CMyClass();
        __mapObjectData.insert( _iObjectId, pClassInstance );
    }

    return ( pClassInstance );
}

My question is about using exceptions for what clearly aren't 'exceptional' conditions. 我的问题是关于明显不是“特殊”条件的例外情况。 It seems like a very concise way of achieving the purpose at hand, rather than involving setting up iterators. 它似乎是实现手头目的的一种非常简洁的方式,而不是涉及设置迭代器。

Are there any gotchas about using exceptions for this type of purpose that I might be missing? 对于我可能会遗漏的此类目的使用异常是否有任何问题?


Measurements* 测量*

So to follow up, I did some performance tests comparing my exception based code to the code example in the selected answer. 因此,为了跟进,我做了一些性能测试,将基于异常的代码与所选答案中的代码示例进行比较。 It was a good exercise to go through and qualifies the suggestions of bad performance using exceptions for the normal flow of code. 通过使用正常代码流的异常来验证不良性能的建议是一个很好的练习。 It also gave some good reference material for the assertion of near zero performance loss when exceptions aren't thrown. 它还提供了一些很好的参考资料,用于在不抛出异常时断言接近于零的性能损失。

Although these tests weren't exhaustive, here's what I observed: 虽然这些测试并非详尽无遗,但我观察到的是:

Each test was run over a large number of insert/fetches using rand() as the source of the map key (which generated the maximum 32768 elements): 每个测试都使用rand()作为映射键的源(生成最大32768个元素)在大量插入/提取上运行:

Exception method: average 5.99 seconds 例外方法:平均5.99秒
Find/add method: average 0.75 seconds 查找/添加方法:平均0.75秒

Increasing the scope of the random elements tenfold gave back these numbers: 将随机元素的范围扩大十倍可以得到这些数字:

Exception method: average 56.7 seconds 例外方法:平均56.7秒
Find/add method: average 4.54 seconds 查找/添加方法:平均4.54秒

I then prefilled the map with all possible key entries so that the try never threw: 然后我用所有可能的密钥条目预填充地图,以便尝试从不抛出:

Exception method: average 0.162 seconds 例外方法:平均0.162秒
Find/add method: average 0.158 seconds 查找/添加方法:平均0.158秒

Curiously, with MS VStudio, debug mode code was faster with the exception handling method. 奇怪的是,使用MS VStudio,异常处理方法的调试模式代码更快。

Compilers typically use a strategy for implementing exceptions that has zero runtime overhead as long as none are thrown, but if an exception is thrown, then it affects your program's performance due to the exception processing mechanism which must unwind the stack. 编译器通常使用策略来实现只有没有抛出任何运行时开销的异常,但如果抛出异常,则由于必须解除堆栈的异常处理机制,它会影响程序的性能。

Even if this is acceptable for your use case, there's simply no advantage to using exceptions to manage control flow in your case. 即使这对于您的用例是可接受的,在您的情况下使用异常来管理控制流也没有任何优势。 Using map::find is not only more succinct, but also more idiomatic. 使用map::find不仅更简洁,而且更具惯用性。

auto iter = _mapObjectData.find(_iObjectId);
if(iter == _mapObjectData.end()) {
  auto instance = new CMyClass();
  _mapObjectData.insert(std::make_pair(_iObjectId, instance));
  return instance;
}
return iter->second;

@Mehrdad has a good suggestion in the comments to use map::lower_bound to locate the key instead of map::find . @Mehrdad在评论中有一个很好的建议 ,使用map::lower_bound来定位键而不是map::find The benefit would be that if the key doesn't exist the return value can be used as a hint for map::insert , which should result in better insertion performance. 好处是如果密钥不存在,则返回值可以用作map::insert提示 ,这将导致更好的插入性能。

auto iter = _mapObjectData.lower_bound(_iObjectId);
if(iter == _mapObjectData.end() || iter->first != _iObjectId) {
  auto instance = new CMyClass();
  _mapObjectData.insert(iter, std::make_pair(_iObjectId, instance));
  return instance;
}
return iter->second;

I also strongly recommend changing the type of your map from 我还强烈建议您更改地图类型

std::map<int, CMyClass*>

to

std::map<int, std::unique_ptr<CMyClass>>

Sticking raw pointers that own resources into standard library containers is usually more trouble than it's worth. 将拥有资源的原始指针粘贴到标准库容器中通常比它的价值更麻烦。

Exception are a means of cleanly separating failure handling from normal case code. 例外是将故障处理与正常案例代码完全分开的一种方法。

In your code they're used for normal case, which defeats the purpose and has no advantage. 在你的代码中,它们被用于正常情况,这会破坏目的而没有任何优势。

In the particular case at hand use [] indexing, which automatically inserts the key if it isn't there already. 在特定情况下,使用[]索引,如果密钥已经存在则会自动插入密钥。 And more generally use conditional constructs for simple conditional control flow. 更常见的是使用条件结构来实现简单的条件控制流程。 There are some exceptional cases where exceptions make sense for expressing normal case control flow (eg returning a result from a deeply nested recursive call), in the sense that the code can become simpler and more clear, but these exceptional cases are … exceptional. 一些特殊情况下的例外是有意义表达正常的情况下,控制流(如返回从一个深度嵌套的递归调用的结果),在这个意义上的代码可以变得更简单,更清晰,但这些例外的情况是例外...。


Regarding efficiency, throwing an exception is costly because C++ compilers are optimized for using exceptions only for failure handling, which is assumed to be rare . 关于效率,抛出异常是非常昂贵的,因为C ++编译器针对故障处理使用异常进行了优化,这被认为是罕见的

When failure becomes the norm, reconsider the working definition of failure. 当失败成为常态时,重新考虑失败的工作定义。

However, just having the possibility of an exception being thrown has very little, down to 0, overhead. 但是,只是抛出异常的可能性很小,低至0,开销。 So you should not be afraid of using exceptions to get that failure handling safely and non-distractingly tucked away from the normal case code. 因此,您不应该害怕使用异常来安全地处理失败,并且不会分散注意力,使其远离正常的案例代码。 Used properly, with this division of code into normal case and failure, exceptions are win-win. 正确使用,将这种代码划分为正常情况和失败,例外是双赢的。


In a comment to another answer you remark, 评论你回答的另一个答案时,

Although I like the brevity of this, it potentially leaves the map with a nullptr if the class can't be instantiated. 虽然我喜欢这种简洁,但如果无法实例化类,它可能会使地图留下nullptr。

Well that is a failure situation, where using exception handling is the correct approach. 那么是一个失败的情况下,在使用异常处理是正确的做法。

For example, 例如,

Your_class* creative_at( int const id, std::map<int, YourClass*>& object_data )
{
    // Basic non-creating at:
    {
        auto const it = object_data.find( id );
        if( it != object_data.end() ) { return it->second; }
    }

    // Create:
    std::unique_ptr<Your_class> p( new Your_class() );    // May throw.
    object_data[id] = p.get();                            // May throw.
    return p.release();
}

This code is exception based, but there is not a try-catch in sight. 此代码基于异常,但看不到try-catch

Instead of the Java approach of manual try-catch-finally , in C++ one mainly lets destructors do automatic cleanup , such as in this case the std::unique_ptr destructor; 而不是手工try-catch-finally的Java方法,在C ++中主要让析构函数进行自动清理 ,例如在这种情况下std::unique_ptr析构函数; this approach is called RAII , short for Resource Acquisition Is Initialization . 这种方法称为RAII ,是资源获取初始化的缩写。

There is a reason in C++ to avoid using too much exceptions: exception processing is heavy (because all the destructors of local variables in all intermediate call frames have to be executed). C ++中有一个原因可以避免使用太多异常:异常处理很繁重(因为所有中间调用帧中的局部变量的所有析构函数都必须执行)。

In Ocaml exception processing is lightweight, so they are more likely to be used like you suggest. Ocaml中,异常处理是轻量级的,因此它们更有可能像您建议的那样使用。

In your example I would rather use the find member function of std::map 在您的示例中,我宁愿使用std::mapfind成员函数

Yes, because neither propagating the exception nor your code itself is particularly efficient. 是的,因为传播异常和代码本身都不是特别有效。

Ignoring the fact that you shouldn't be storing raw pointers in the first place, the next-most-correct and most efficient (and readable!) way to do what you're doing is to use operator[] : 忽略你不应该首先存储原始指针的事实,你正在做的事情的下一个最正确和最有效 (和可读!)的方法是使用operator[]

CMyClass* CreateAndOrGetClass(int _iObjectId, std::map<int, CMyClass*>& _mapObjectData)
{
    CMyClass* &pClassInstance = _mapObjectData[_iObjectId];
    if (!pClassInstance) { pClassInstance = new CMyClass(); }
    return pClassInstance;
}

This way, the lookup in the map is only done once , and the value is constructed in-place. 这样,地图中的查找只进行一次 ,并且值就地构造。

If course, in reality you should probably just store a CMyClass as the value, not CMyClass * , but that's independent of the point of my answer here. 如果当然,实际上你应该只将CMyClass存储为值,而不是CMyClass * ,但这与我的答案无关。

As noted by others, exceptions are slow then thrown/catched. 正如其他人所指出的那样,异常是缓慢的然后抛出/捕获。

But it does not mean the alternative must be ugly. 但这并不意味着替代方案必须是丑陋的。

if(!mymap.count(objectId))
{

}

Tells you whether your object is missing. 告诉您对象是否丢失。

Also, are you sure you need pointers at all for the map values? 另外,你确定你需要指针来完成地图值吗? The only resons to do that is if your class' copy constructor is very slow, or the class is not copyable/movable. 这样做的唯一原因是你的类的复制构造函数非常慢,或者类是不可复制/可移动的。 Even in that case, you'll want to use unique-ptr or shared-ptr 即使在这种情况下,您也会想要使用unique-ptrshared-ptr

Also, your version of hungarian notation, with prefixes like C, underscore i, p, etc is becoming less and less popular. 此外,您的匈牙利表示法版本,前缀为C,下划线i,p等正变得越来越不受欢迎。 Many people recognize it's more trouble than help. 许多人认识到这比帮助更麻烦。 See Alexandescu & Sutter's C++ Coding standards, Chapter 0. 参见Alexandescu&Sutter的C ++编码标准,第0章。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM