The attitude of C++ developers towards exception is divided. One group thinks it is a good feature, which provides a different mechanism for defense programming and catching user errors. Another group thinks it should be definitely banned, due to its immature functionality and code compatibility. Immature functionality like code bloating, performance penalty and lose control to compiler. Code compatibility is a good excuse for banning it as well, because most C++ developers come out of C background. And C does not provide exception and it is not bad decision to ban exception if a large legacy code base is based on C. Some giant like Google bans exception in Google C++ coding standard. In my opinion C++ exception is a reasonable tool to catch user errors (better than error code in my experience), which is to provide feedback to user interaction with UI. But do pay attention when the code is well modulized and especially be careful when exceptions are allowed to across the boundary of modules.
If in your application you decide to use exception for any good reason, be careful when catching them especially when there is class hierarchy among user-defined exceptions. I going to talk about a few scenarios caused me a bit headache in my previous projects, which I would like share with you.
1. Catch by value for build-in types otherwise catch by reference
When an object is thrown in the try block, an copy is actually created and caught in the catch block. For build-in types, as the fundamental building blocks of C++ language, are quite cheap to be copied and no performance penalty to do so.
However for user-defined class/struct it might not be the case. Creating temporary objects, passing around and eventually destroyed. There is not only performance penalty but also object-slicing if catching by object when there is inheritance existing. Which is better, catch by reference or catch by pointer? Catch by reference provides the exactly same functionality as catch by pointer, and has one advantage of no need worrying about when to release the memory. So most of giants in C++ prefers catch by reference.
Catch by object: object slicing and non-polymorphic
*********************************************************************************
class BaseException : public std::exception {
public:
virtual const char* what() const;
private:
// data member
};
class DerivedException : public BaseException {
public:
virtual const char* what() const;
private:
// more data memebr
}
void CatchExceptionByObject()
{
try {
throw DerivedException();
} catch (BaseException be) {
// clean up (anything want to be done)
be.what();
} cathc (...) {
// any other
}
}
*********************************************************************************
As I talked above, in CatchExceptionByObject() DerivedException is thrown but caught by its base class object. Here the object slicing will happen when creating the copy object from DerivedException to BaseException. (The copy constructor of BaseException will be called with the argument of reference to DerivedException). Therefor be.what() will be calling BaseException::what(). Obviously it is not what we
intend to do.
Catch by reference: polymorphic and no object slicing
*********************************************************************************
void CatchExceptionByObject()
{
try {
throw DerivedException();
} catch (BaseException& be) {
// clean up (anything want to be done)
be.what();
} cathc (...) {
// any other
}
}
*********************************************************************************
Firstly catch by reference is cheap to make a copy of reference and secondly there is no object slicing. This means that be.what() will be calling DerivedException::what(). Looks good to me!
2. No implicit type promotion and implicit type conversion
When coming to try-catch block, something used to work as you though like object promotion and implicit type conversion will stop working on exception.
No implicit type promotion and conversion
*********************************************************************************
class IntegerWrapper {
public:
IntegerWrapper(int i) : m_i(i)
{}
private:
int m_i;
};
void CatchExceptionNoTypePromotionAndConversion()
{
float i = 1; // OK: integer "1" is promoted to float
IntegerWrapper intWrapper = 2; // OK: implicit type conversion
// IntergerWrapper::IntegerWrapper(int) is called
try {
throw 1;
} catch (float) {
std::cout << "CatchExceptionNoTypePromotion::folat" << std::endl;
} catch (IntegerWrapper& iw) {
std::cout << "CatchExceptionNoTypePromotion::IntegerWrapper" << std::endl;
} catch (...) {
std::cout << "CatchExceptionNoTypePromotion::anything else" << std::endl;
}
}
*********************************************************************************
In function CatchExceptionNoTypePromotionAndConversion(), it is surprising that neither catch(float) is hit nor catch(IntegerWrapper&) is hit. It actually hits catch(...) block. Here the type promotion/conversion from int to float/IntegerWrapper does not happen at all. Exception is static-linked or called dumb. Neither of them will work. It only looks for the exact type-matching in catch blocks.
Correct implementation: catch by the exact type
*********************************************************************************
void CatchExceptionNoTypePromotionAndConversion()
{
try {
throw 1;
} catch (int i) {
std::cout << "CatchExceptionNoTypePromotion::int" << std::endl;
} catch (...) {
std::cout << "CatchExceptionNoTypePromotion::anything else" << std::endl;
}
try {
throw 1.0f;
} catch (float f) {
std::cout << "CatchExceptionNoTypePromotion::float" << std::endl;
} catch (...) {
std::cout << "CatchExceptionNoTypePromotion::anything else" << std::endl;
}
try {
throw IntegerWrapper(1);
} catch (IntegerWrapper& iw) {
std::cout << "CatchExceptionNoTypePromotion::IntegerWrapper" << std::endl;
} catch (...) {
std::cout << "CatchExceptionNoTypePromotion::anything else" << std::endl;
}
}
*********************************************************************************
The type promotion and implicit type conversion stop working when coming to exception catching. The only type conversion allowed on exception is based on inheritance hierarchy, from derived class to base class.
3. Catch most derived type first
Of course here I am talking about the user-defined class/struct. Among build-in types there is no inheritance at all. And they are all independent types.
In catch-blocks C++ language is not to find the most fit catch block (not like the function argument deduction on function overloading or template matching, always find the closest match), but to find the very first match, which may not be the closest match. For instance throwing a derived class reference, the will be caught in the base class reference if the base class catch block appears before the derived class block.
Catch by pointer: surprise caused by the order of catch blocks
*********************************************************************************
void CatchExceptionClassHiearachyByPointer()
{
try {
throw new DerivedException();
} catch (void* e) {
std::cout << "CatchExceptionClassHiearachyByPointer::void *" << std::endl;
} catch (BaseException* be) {
be.what();
} catch (DerivedException* de) {
de.what();
} catch (...) {
std::cout << "CatchExceptionClassHiearachyByPointer::anything else" << std::endl;
}
}
*********************************************************************************
In this code snippet a DerivedException pointer is thrown and caught. But surprisingly it will not be caught either as BaseException* or as DerivedException*, but caught as void*. Remember that "void*" is at the top of hierarchy of any type of pointers. It means that any type of pointer can be caught as void* and will be caught as void* if the catch block of void* appears on the top of other pointer types.
If I removed the catch block of void* in the above code snippet, the exception will not be caught in DerivedException* either. It will be caught as its base class BaseException*, because its base class catch block appear before its own catch block.
Catch by pointer: the most derived first
*********************************************************************************
void CatchExceptionClassHierarchyByPointer()
{
try {
throw new DerivedException();
} catch (DerivedException* de) {
de.what();
} catch (BaseException* be) {
be.what();
} catch (void* e) {
std::cout << "CatchExceptionClassHierarchyByPointer::void *" << std::endl;
} catch (...) {
std::cout << "CatchExceptionClassHierarchyByPointer::anything else" << std::endl;
}
}
*********************************************************************************
The most derived catch block should be stay at the top as well, if catching by reference. It works the exactly same as catch by pointer.
Catch by reference: the most derived first
*********************************************************************************
void CatchExceptionClassHierarchyByReference()
{
try {
throw DerivedException();
} catch (DerivedException* de) {
std::cout << "CatchExceptionClassHierarchyByReference::DerivedException &" << std::endl;
} catch (BaseException& be) {
std::cout << "CatchExceptionClassHierarchyByReference::BaseException &" << std::endl;
} catch (...) {
std::cout << "CatchExceptionClassHierarchyByReference::anything else" << std::endl;
}
}
*********************************************************************************
Both reference and pointer types work well on exception when coming to the type conversion. But prefer catch by reference than catch by pointer, as I talked in Section 1,
4. Implement a virtual function, Rethrow(), if re-throw polymorphically
This is one of troubles that I encountered in one of my previous projects. What I need to do is to catch a polymorphic exception in one module and do some work with this exception, then re-throw this exception polymorphically. It will be caught again in the upper-level module, which has to know the exact type of exception and provide useful information to users, for instance by throwing an user-type exception.
Scenarios in my previous project
*********************************************************************************
class MyDerivedException : public DerivedException {
public:
virtual const char* what() const {
return "MyDerivedException is thrown";
}
};
//************************* Lower-level module **********************
// In the lower-level module
// catch the exception and do some work with the
// exception and re-throw it. And it will be
// caught in its upper-level module
void HandleExceptionAndRethrow(BaseException& be)
{
std::cout << be.what() << std::endl;
/*
* more job to be done
*/
// re-throw the exception to the upper-level module
throw be;
}
void CatchExceptionAndRethrow()
{
try {
throw MyDerivedException();
} catch (MyDerivedException& mde) {
HandleExceptionAndRethrow(mde);
} catch (DerivedException& de) {
HandleExceptionAndRethrow(de);
} catch (BaseException& be) {
HandleExceptionAndRethrow(be);
} catch (...) {
std::cout << "CatchExceptionAndRethrow: anything else"
<< std::endl;
}
}
//************************* Upper-level module **********************
// In the upper-level module
// catch the exception from lower-level model
// do some work and re-throw user-type exception to
// provide better UI experience
void CatchExceptionAndRethrowUserException()
{
try {
/*
* call functions of lower-level module
*/
} catch (MyDerivedException& mde) {
/*
* any job done here and re-throw user-type exception
*/
throw UserException1();
} catch (DerivedException& de) {
/*
* any job done here and re-throw user-type exception
*/
throw UserException2();
} catch (BaseException& be) {
/*
* any job done here and re-throw user-type exception
*/
throw UserException3();
} catch (...) {
std::cout << "CatchExceptionAndRethrow: anything else"
<< std::endl;
}
}
*********************************************************************************
The problem happens in the lower-level module, HandleExceptionAndRethrow((BaseException&).There is no problem with how this function is called. And the argument passed to it is actually correct and polymorphic types. For instance calling "be.what()" is the polymorphic behavior. So far so good.
However very surprisingly no matter what exception is re-thrown in the lower-level module in HandleExceptionAndRethrow((BaseException&), always BaseException& is caught in the upper-level module. What is wrong? The problem starts when re-throwing the exception in HandleExceptionAndRethrow(BaseException&) . The root cause is that re-throwing from a base class polymorphically does not work. Always the base class is re-thrown no matter what the actual type is. Re-throw an object is static-linked and never checks its type in run-time. In HandleExceptionAndRethrow(BaseException&) an object of BaseException is created and re-thrown.
The solution is to implement an virtual function Rethrow(), simply throw itself.
Solution:
*********************************************************************************
class BaseException : std::exception {
public:
virtual const char* what() const {
return "BaseException is thrown";
}
virtual void ReThrow() {
throw *this;
}
};
class DerivedException : public BaseException {
public:
virtual const char* what() const {
return "DerivedException is thrown";
}
virtual void ReThrow() {
throw *this;
}
};
class MyDerivedException : public DerivedException {
public:
virtual const char* what() const {
return "MyDerivedException is thrown";
}
virtual void ReThrow() {
throw *this;
}
};
void HandleExceptionAndRethrow(BaseException& be)
{
std::cout << be.what() << std::endl;
be.ReThrow();
}
*********************************************************************************
By implementing a virtual function that is to re-throw itself, it will make sure its actually type of object is created and re-thrown. Therefore re-throwing polymorphically will work.
Summary
Exception is a reasonable feature to provide good error-catching mechanism. Just keep a few things in mind when using them.
- Catch by value for build-in types
- Catch by reference for class/struct
- Implicit type promotion and conversion does not apply to exception
- Catch the most derived at the top
- Implement a Rethrow() function polymorphically
No comments:
Post a Comment