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.
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 (e.g. 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With