简体   繁体   中英

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.

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):

Exception method: average 5.99 seconds
Find/add method: average 0.75 seconds

Increasing the scope of the random elements tenfold gave back these numbers:

Exception method: average 56.7 seconds
Find/add method: average 4.54 seconds

I then prefilled the map with all possible key entries so that the try never threw:

Exception method: average 0.162 seconds
Find/add method: average 0.158 seconds

Curiously, with MS VStudio, debug mode code was faster with the exception handling method.

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.

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 . 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.

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 .

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. 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.

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.

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; this approach is called RAII , short for Resource Acquisition Is Initialization .

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).

In Ocaml exception processing is lightweight, so they are more likely to be used like you suggest.

In your example I would rather use the find member function of std::map

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[] :

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.

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

Also, your version of hungarian notation, with prefixes like C, underscore i, p, etc is becoming less and less popular. Many people recognize it's more trouble than help. See Alexandescu & Sutter's C++ Coding standards, Chapter 0.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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