Firstly for most C++ applications encountering exceptions seems inevitable, because some of most fundamental operations are embedded with exceptions, such as std::bad_alloc when allocating memory in heap and std::bad_cast in dynamic casting, not even mentioning Standard Template Library. Hence completely removing C++ exception features and using error code only seems no longer a valid option for most C++ applications.
Secondly utilizing exceptions in C++ is such an easy/convenient option when software application involves user interaction, in order to respond with meaningful and helpful feedback to users. Comparing with error-code approach in multiple software modules, exception approach is more flexible to provide multiple levels of logging/debugging facilities and recover from user errors.
Of course misusing exceptions is error-prone as well especially across the boundary of modules, given the fact that currently C++ exception specification is not very friendly. I found that one occasion is very error prone, most of time causing unnecessary software crash. Introducing new exceptions where its host function is called a lot of places, for instance compiled as static library and used in multiple modules as a par of DLLs or dynamic libraries. Therefore it is not easy job to capture this exception properly in all occasions, especially this exception can be propagated into its caller modules, as this same exception may need variable user feedback/interactions. In a few cases I found that I have to analyze the code path in order to make sure the newly introduced exceptions are caught in all possibilities.
But this does not prevent the legitimate use of exceptions. In my experience it is vital to combine both exception and error-code approach. Apply error-code approach to internal error and throw exceptions only for user errors. Another good practice to limit the exceptions thrown across the models and provide good documentation at least. As currently C++ exception specification is not very friendly. Use good documentation style and naming convention to remind developers propagating exceptions. (Some good practice about exception please refer to Herb Sutter's book, like exception level and single responsibility)
Here I would like to learn function-try-block together with you. Go through its key points with some code snippets.
1. Used with constructor
Constructor is one of the special functions in C++. It has global, non-virtual and static semantics because it can be called anyway in the code given the assumption that it is has public accessibility and the header of its declaration is included. What's more it has call hierarchy to follow when the derived class constructor is called (please refer to The order of object initialization/destruction). This call hierarchy is defined by the standard and the compiler has to do the tricks to meet the standard. Therefore there is something in hidden that has to be done by the compiler.As a matter of fact of this hidden code is out of control of developers what if the hidden-code/call-hierarchy throws exception before reaching the constructor body. With the conventional try-block there is no way to catch it and therefore developers have to write specific code to manage the potential resource leaking.
With the introduction of function-try-block, it can be used with constructor's initialization list, such as base class initialization and member variables initialization. It can catch the exceptions thrown in these code. More importantly it guarantees that all the well-constructed base class object and member variables will be destroyed before hitting the catch-section.
What purpose is function-try-block used for together with constructor? Or what can we achieve if the exception is thrown and caught in the constructor's initialization list. In my personal experience its main purpose servers for the debugging/logging facility and providing user feedback via exception in different levels and across modules, if some critical requirements in constructing objects is violated and triggers the exception. In this blog on drdobbs, it provides another scenario for function-try-block used with copy constructor when the creation constructor is too expensive for the purpose of the code efficiency and performance. It is a valid point if the code design demonstrated in the example is the only choice. (This validity of this example is arguable. In my opinion any creation constructor should not be expensive at the first place. In this blog on drdobbs its object construction should be tailored into two parts. The first part should be exception free in constructor for inexpensive initialization and the second part for the expensive initialization should be implemented as a public function, for such a "Init()" function.)
Base-derived constructors:
All the well-constructed member variables are to be destroyed before hitting the catch-section in the function-try-block in constructor. All the well constructed based classes and their member variables are to be destroyed too before hitting the catch-section in the derived class.
//********************************************************************************
class MyException : public std::exception {
public:
MyException(const std::string& msg) : m_Msg(msg)
{}
virtual ~MyException() {}
// MSVC12 does not support noexcept yet
virtual const char* what() const throw() {
return m_Msg.c_str();
}
void AppendMessage(const std::string& msg) {
m_Msg.append(msg);
}
protected:
std::string m_Msg{ "" };
};
class CreationException : public MyException {
public:
CreationException(const std::string& msg)
: MyException("CreationException: ")
{
m_Msg.append(msg);
}
};
class DestructionException : public MyException {
public:
DestructionException(const std::string& msg)
: MyException("DestructionException: ")
{
m_Msg.append(msg);
}
};
class MyNonEmptyString {
public:
MyNonEmptyString(const std::string& str)
: m_Str(str) {
if (m_Str.empty()) {
throw CreationException("MyNonEmptyString");
}
}
~MyNonEmptyString() {
std::cout << "~MyNonEmptyString()" << std::endl;
}
private:
std::string m_Str;
};
class MyString {
public:
MyString(const std::string& str)
: m_Str(str) {
}
~MyString() {
std::cout << "~MyString()" << std::endl;
}
private:
std::string m_Str;
};
class Base {
public:
Base(const std::string& name, const int* val)
try : m_Name(name), m_ValPtr(val) {
if (m_ValPtr == nullptr) {
throw CreationException("Base");
}
}
catch (CreationException& ce){
std::cout << ce.what() << std::endl;
}
virtual ~Base() {
std::cout << "~Base()" << std::endl;
}
private:
MyNonEmptyString m_Name;
const int* m_ValPtr;
};
class Derived : public Base {
public:
Derived(const std::string& name, const int* valPtr, const double* data)
try : Base(name, valPtr), m_Data(data) {
if (m_Data == nullptr) {
throw CreationException("Derived");
}
}
catch (CreationException& ce) {
std::cout << ce.what() << std::endl;
}
~Derived() {
std::cout << "~Derived()" << std::endl;
}
private:
MyString m_Version{ "1.0" };
const double* m_Data;
};
// test
int _tmain(int argc, _TCHAR* argv[])
{
try {
Base b1(std::string(""), nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating Base b1: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating Base b1: " << "unknown" << std::endl;
}
try {
Base b2(std::string("Base"), nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating Base b2: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating Base b2: " << "unknown" << std::endl;
}
try {
Derived d1(std::string(""), nullptr, nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating Derived d1: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating Derived d1: " << "unknown" << std::endl;
}
try {
Derived d2(std::string("Derived"), nullptr, nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating Derived d2: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating Derived d2: " << "unknown" << std::endl;
}
try {
int intBuf[2];
Derived d3(std::string("Derived"), intBuf, nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating Derived d3: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating Derived d3: " << "unknown" << std::endl;
}
return 0;
}
// Output
//********************************************************************************
In the creation of "Derived d1" the creation exception thrown in MyNonEmptyString was caught 3 times in the constructor of Base, Derived and main(). The exception caught in function-try-block in constructor will be automatically re-thrown and the caller function to create the object has to catch the exception. Any logging/debugging information can be closely located in the catch-section together with the constructor. Easy to maintain and read.
In the creation of "Derived d3" before hitting catch-section of Derived constructor the member variables of its base class, its base class and its own member variables are all destroyed. This behavior is exactly what we wanted.
Delegate/target constructors:
I have briefly talked about delegate/target constructor and the motivation behind it. Please refer to C++11 features - Improvement on object construction. Using function-try-block in delegate constructor will guarantee that the well-constructed object and its member variables by target constructor are to be destroyed before hitting the catch-section in the delegate constructor.
//********************************************************************************
class BaseEx {
public:
// target constructor
BaseEx(const std::string& name, const int* val)
: m_Name(name), m_ValPtr(val) {
if (m_ValPtr == nullptr) {
throw CreationException("BaseEx");
}
}
// delegate constructor
BaseEx(const int* val)
try : BaseEx("BaseEx", val) {
}
catch (CreationException& ce) {
ce.AppendMessage("-Invalid pointer");
std::cout << ce.what() << std::endl;
}
virtual ~BaseEx() {
std::cout << "~BaseEx()" << std::endl;
}
private:
MyNonEmptyString m_Name;
const int* m_ValPtr;
};
class DerivedEx : public BaseEx {
public:
DerivedEx(const std::string& name, const int* valPtr, const double* data)
: BaseEx(name, valPtr), m_Data(data) {
if (m_Data == nullptr) {
throw CreationException("DerivedEx");
}
}
~DerivedEx() {
std::cout << "~Derived()Ex" << std::endl;
}
private:
MyString m_Version{ "1.0" };
const double* m_Data;
};
// test
int _tmain(int argc, _TCHAR* argv[])
{
try {
BaseEx bex1(nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating BaseEx bex1: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating BaseEx bex1: " << "unknown" << std::endl;
}
try {
DerivedEx dex1(std::string(""), nullptr, nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating DerivedEx dex1: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating DerivedEx dex1: " << "unknown" << std::endl;
}
try {
DerivedEx dex2(std::string("DerivedEx"), nullptr, nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating DerivedEx dex2: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating DerivedEx dex2: " << "unknown" << std::endl;
}
try {
int intBuf[2];
DerivedEx dex3(std::string("DerivedEx"), intBuf, nullptr);
}
catch (CreationException& ce) {
std::cout << "Exception caught in creating DerivedEx dex3: "
<< ce.what()
<< std::endl
<< std::endl;
}
catch (...) {
std::cout << "Exception caught in creating DerivedEx dex3: " << "unknown" << std::endl;
}
return 0;
}
// output
//********************************************************************************
In the creation "BaseEx bex1" the CreationException thrown in target constructor is caught in delegate constructor. As expected its member variables are destroyed before hitting the catch-section.
Bear in mind that the exception is caught in catch-section of function-try-block used together with constructor/destructor will be automatically re-thrown. And for constructor particularly there cannot be a return statement in function-try-block.
2. Rarely used with destructor
Destructor is another special function in C++. It has global, static and virtual semantics. Its calling hierarchy is the opposite order of the creation constructor. It has its special personality when handling the exception. If an exception caught in destructor, stack-unwinding is triggered. At this time if another exception is thrown during the stack unwinding, normally terminate() will be called and the program is to terminate.
So it is never a good practice to thrown exception in destructor. And function-try-block is not to help this special occasions either. If for some logging/debugging requirement throwing exceptions is a must design requirement, then keep in mind that do not let exception propagate.
//********************************************************************************
class BadInt {
public:
BadInt(int val) : m_val(val)
{}
~BadInt() {
throw DestructionException("BadInt");
}
private:
int m_val;
};
class BaseFool {
public:
virtual ~BaseFool() {
std::cout << "~BaseFool()" << std::endl;
}
private:
MyString m_Version{ "1.0" };
};
class Fool1 : public BaseFool {
public:
~Fool1() {
std::cout << "~Fool1()" << std::endl;
}
public:
BadInt m_val{ 0 };
MyNonEmptyString m_ID{ "Fool1" };
};
class Fool2 : public BaseFool{
public:
~Fool2() try {
std::cout << "~Fool2()" << std::endl;
}
catch (DestructionException& de) {
std::cout << "Exception throwin in ~Fool2(): " << de.what() << std::endl;
}
public:
BadInt m_val{ 0 };
MyNonEmptyString m_ID{ "Fool2" };
};
int _tmain(int argc, _TCHAR* argv[])
{
try {
Fool1 f;
}
catch (DestructionException& de){
std::cout << "Exception caught: "
<< de.what()
<< std::endl
<< std::endl;
}
try {
Fool2 f;
}
catch (DestructionException& de){
std::cout << "Exception caught: "
<< de.what()
<< std::endl
<< std::endl;
}
return 0;
}
// output
//********************************************************************************
In the destruction of "Fool2 f" itself, its member variables, base class, and member variables of base class are all destroyed before hitting the catch-section. As stated in Section 1 the exception caught in function-try-block with destructor will be automatically re-thrown too. Therefore the caller function need to catch this exception.
3. Do not use with normal functions
C++11 standard says that function-try-block can be used for any function. But because normal function does not have the hidden code like constructor and destructor. Therefore anything can be captured by function-try-block can be captured by try-block.
For me both options are valid in terms of their functionality. However I would prefer the try-block for normal function, as it is easier to read and keeps the back compatibility.
4. Do not use with main()
This has been one of most frequently asked questions for function-try-block. In theory as I described in Section 3, the standard does not prevent developers from using function-try-block with main(). However the drive behind this question is that developers are trying to use function-try-block to capture the exceptions thrown from the creation/initialization of global variables or static variables, like global singleton object, under scope or namespace in main() function. Hence some debugging facility can be logged.
Unfortunately the answer is no. One of good explanations of why it is a negative answer, especially on the static variables under scope and namespace, is that the standard does not guarantee that the static variables under scope or namespace initialized in certain order and does not guarantee that they are initialized/created before entering main(), as C++ has lazy initialization idiom.
//********************************************************************************
MyNonEmptyString globalStr("");
namespace MySocpe {
MyNonEmptyString globalStr("");
static MyNonEmptyString staticStr("");
}
int _tmain(int argc, _TCHAR* argv[]) try {
std::cout << "Main()" << std::endl;
return 0;
}
catch (CreationException& ce){
std::cout << "Exception caught in global variables: "
<< ce.what()
<< std::endl;
}
//********************************************************************************
This tiny snippet of code will crash before hitting main(). And it will disappoint you if you are expecting something to print out from either MyNonEmptyString or the catch-section of main(), as it is a straightforward crash and nothing to print out.
5. Life time of arguments
The arguments in function-try-block can be accessed both in try-section and catch-section, which is the same as the normal try-block. This poses the difference against the life time of member variables.
6. Life time of member variables
Member variables are only accessible in try-section right before the exception is thrown and they are not accessible in catch-section. This is the right behavior that we want. The standard guarantees that all the well-constructed member variables are destroyed when hitting catch-section. And the base class object and its member variables are destroyed too if the exception is thrown and caught in function-try-block in the derived class.
Again as stated in Section 1 the the object and its member variables will be destroyed as well if the exception is thrown and caught in delegated constructor (the life time of this object starts from a successful returning from the target constructor).
7. Can't catch exceptions thrown by static variables in namespace or scope
Yet again as I briefly mentioned this item in Section 4. Because the standard does not guarantee the order of static variables under scope or namespace. We already know that the memory of global/static variables is allocated in global/static memory section but there is no guarantee that they are initialized/created before entering main(). Either we do not know when exactly it will be firstly initialized/created in our program, because C++ has lazy initialization idiom.
Even though developers can reason out when the first time the object associated/related/used with the static variables is initialized/created (the lazy initialization is fired). And hence can put a function-try-block around it. Still this makes no positive impact or it is nearly not maintainable if later other developers create this object earlier and this function-try-block has to move together with it. It means that every time the reasoning for the first time initialization/creation of the object associated with static variables has to be done before modifying the code. I can't image how this can be achieved.
//********************************************************************************
class Bar {
public:
Bar() {}
private:
MyString m_ID{ "Bar" };
// Crash at beginning of the programm, no matter
// if any insatnce of Bar is created or not.
static MyNonEmptyString m_NEStr;
};
MyNonEmptyString Bar::m_NEStr{ "" };
int _tmain(int argc, _TCHAR* argv[])
{
try {
Bar b;
}
catch (CreationException& ce) {
std::cout << "Exception caught in Bar b: " << ce.what() << std::endl;
}
return 0;
}
This exception thrown by Bar::m_NEStr can be caught neither by function-try-block, as shown in Section 4, nor by normal try-block as above code snippet.
8 . Can catch exceptions thrown by static variable in function at initialization
As I described in above section, the exception thrown from the initialization/creation of static variables under scope or namespace cannot be caught by function-try-block, because we don't know when exactly the lazy initialization is fired and hence not knowing where to put the function-try-block.
However for static variables in a function the exception thrown from their initialization/created can be caught by function-try-block, because we know exactly when the lazy initialization is fired - the first time enters the nearest scope of the function.
//********************************************************************************
int Foo() try {
static MyNonEmptyString localString("");
//MyNonEmptyString localString("");
return 2;
}
catch (CreationException& ce) {
std::cout << "Exception caught in local static variables: "
<< ce.what()
<< std::endl;
}
class Bar {
public:
Bar()
try {
static MyNonEmptyString localStaticStr("");
}
catch (CreationException& ce) {
std::cout << ce.what() << std::endl;
}
private:
MyString m_ID{ "Bar" };
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo();
try {
Bar b;
}
catch (CreationException& ce) {
std::cout << "Exception caught in Bar b: " << ce.what() << std::endl;
}
return 0;
}
// output:
//********************************************************************************
In this example the exception thrown from the static variable in normal function or constructor can be caught properly.
Summary:
Function-try-block has a valid user case with constructor. It guarantees that all the well-constructed objects at the time throwing the exceptions will be destroyed before hitting the catch-section. This brings two advantages. One is to guarantee no resource leaking and the second is code cohesion, to take any functionality in catch-section close to the exception thrown.
And it is not a good practice to use function-try-block with any another functions, destructor, main() and normal function.
Bibliography:
[1] C++11 Standard
[2] http://www.stroustrup.com/C++11FAQ.html
[3] http://www.drdobbs.com/cpp/understanding-c-function-try-blocks/240168262
No comments:
Post a Comment