Up to this point

来源:互联网 发布:怎么给淘宝客服发文件 编辑:程序博客网 时间:2024/04/29 13:02

Up to this point in the tutorial, you’ve only seen exceptions used in non-member functions. However, exceptions are equally useful in member functions, and even moreso in overloaded operators. Consider the following overloaded [] operator as part of a simple integer array class:

1
2
3
4
intIntArray::operator[](constint nIndex)
{
    returnm_nData[nIndex];
}

Although this function will work great as long as nIndex is a valid array index, this function is sorely lacking in some good error checking. We could add an assert statement to ensure the index is valid:

1
2
3
4
5
intIntArray::operator[](constint nIndex)
{
    assert(nIndex >= 0 && nIndex < GetLength());
    returnm_nData[nIndex];
}

Now if the user passes in an invalid index, the program will cause an assertion error. While this is useful to indicate to the user that something went wrong, sometimes the better course of action is to fail silently and let the caller know something went wrong so they can deal with it as appropriate.

Unfortunately, because overloaded operators have specific requirements as to the number and type of parameter(s) they can take and return, there is no flexibility for passing back error codes or boolean values to the caller. However, since exceptions do not change the signature of a function, they can be put to great use here. Here’s an example:

1
2
3
4
5
6
7
intIntArray::operator[](constint nIndex)
{
    if(nIndex < 0 || nIndex >= GetLength())
        thrownIndex;
 
    returnm_nData[nIndex];
}

Now, if the user passes in an invalid exception, operator[] will throw an int exception.

When constructors fail

Constructors are another area of classes in which exceptions can be very useful. If a constructor fails, simply throw an exception to indicate the object failed to create. The object’s construction is aborted and its destructor is never executed.

Exception classes

One of the major problem with using basic data types (such as int) as exception types is that they are inherently vague. An even bigger problem is disambiguation of what an exception means when there are multiple statements or function calls within a try block.

1
2
3
4
5
6
7
8
try
{
    int*nValue = newint(anArray[nIndex1] + anArray[nIndex2]);
}
catch(intnValue)
{
    // What are we catching here?
}

In this example, if we were to catch an int exception, what does that really tell us? Was one of the array indexes out of bounds? Did operator+ cause integer overflow? Did operator new fail because it ran out of memory? Unfortunately, in this case, there’s just no easy way to disambiguate. While we can throw char* exceptions to solve the problem of identifying WHAT went wrong, this still does not provide us the ability to handle exceptions from various sources differently.

One way to solve this problem is to use exception classes. An exception class is just a normal class that is designed specifically to be thrown as an exception. Let’s design a simple exception class to be used with our IntArray class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
classArrayException
{
private:
    std::string m_strError;
 
    ArrayException() {}; // not meant to be called
public:
    ArrayException(std::string strError)
        : m_strError(strError)
    {
    }
 
    std::string GetError() { returnm_strError; }
}

Here’s our overloaded operator[] throwing this class:

1
2
3
4
5
6
7
intIntArray::operator[](constint nIndex)
{
    if(nIndex < 0 || nIndex >= GetLength())
        throwArrayException("Invalid index");
 
    returnm_nData[nIndex];
}

And a sample usage of this class:

1
2
3
4
5
6
7
8
try
{
    intnValue = anArray[5];
}
catch(ArrayException &cException)
{
    cerr << "An array exception occurred (" << cException.GetError() << ")"<< endl;
}

Using such a class, we can have the exception return a description of the problem that occurred, which provides context for what went wrong. And since ArrayException is it’s own unique type, we can specifically catch exceptions thrown by the array class and treat them differently from other exceptions if we wish.

Note that exception handlers should catch class exception objects by reference instead of by value. This prevents the compiler from make a copy of the exception, which can be expensive when the exception is a class object. Catching exceptions by pointer should generally be avoided unless you have a specific reason to do so.

std::exception

The C++ standard library comes with an exception class that is used by many of the other standard library classes. The class is almost identical to the ArrayException class above, except the GetError() function is named what():

1
2
3
4
5
6
7
8
try
{
    // do some stuff with the standard library here
}
catch(std::exception &cException)
{
    cerr << "Standard exception: " << cException.what() << endl;
}

We’ll talk more about std::exception in a moment.

Exceptions and inheritance

Since it’s possible to throw classes as exceptions, and classes can be derived from other classes, we need to consider what happens when we use inherited classes as exceptions. As it turns out, exception handlers will not only match classes of a specific type, they’ll also match classes derived from that specific type as well! Consider the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
classBase
{
public:
    Base() {}
};
 
classDerived: publicBase
{
public:
    Derived() {}
};
 
intmain()
{
    try
    {
        throwDerived();
    }
    catch(Base &cBase)
    {
        cerr << "caught Base";
    }
    catch(Derived &cDerived)
    {
        cerr << "caught Derived";
    }
 
    return0;
}

In the above example we throw an exception of type Derived. However, the output of this program is:

caught Base

What happened?

First, as mentioned above, derived classes will be caught by handlers for the base type. Because Derived is derived from Base, Derived is-a Base (they have an is-a relationship). Second, when C++ is attempting to find a handler for a raised exception, it does so sequentially. Consequently, the first thing C++ does is check whether the exception handler for Base matches the Derived exception. Because Derived is-a Base, the answer is yes, and it executes the catch block for type Base! The catch block for Derived is never even tested in this case.

In order to make this example work as expected, we need to flip the order of the catch blocks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
classBase
{
public:
    Base() {}
};
 
classDerived: publicBase
{
public:
    Derived() {}
};
 
intmain()
{
    try
    {
        throwDerived();
    }
    catch(Derived &cDerived)
    {
        cerr << "caught Derived";
    }
    catch(Base &cBase)
    {
        cerr << "caught Base";
    }
 
    return0;
}

This way, the Derived handler will get first shot at catching objects of type Derived (before the handler for Base can). Objects of type Base will not match the Derived handler (Derived is-a Base, but Base is not a Derived), and thus will “fall through” to the Base handler.

Rule: Handlers for derived exception classes should be listed before those for base classes.

The ability to use a handler to catch exceptions of derived types using a handler for the base class turns out to be exceedingly useful.

Let’s take a look at this using std::exception. There are many classes derived from std::exception, such as std::bad_alloc, std::bad_cast, std::runtime_error, and others. When the standard library has an error, it can throw a derived exception correlating to the appropriate specific problem it has encountered.

Most of the time, we probably won’t care whether the problem was a bad allocation, a bad cast, or something else. We just care that we got an exception from the standard library. In this case, we just set up an exception handler to catch std::exception, and we’ll end up catching std::exception and all of the derived exceptions together in one place. Easy!

1
2
3
4
5
6
7
8
9
try
{
     // code using standard library goes here
}
// This handler will catch std::exception and all the derived exceptions too
catch(std::exception &cException)
{
    cerr << "Standard exception: " << cException.what() << endl;
}

However, sometimes we’ll want to handle a specific type of exception differently. In this case, we can add a handler for that specific type, and let all the others “fall through” to the base handler. Consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
{
     // code using standard library goes here
}
// This handler will catch std::bad_alloc (and any exceptions derived from it) here
catch(std::bad_alloc &cException)
{
    cerr << "You ran out of memory!" << endl;
}
// This handler will catch std::exception (and any exception derived from it) that fall
// through here
catch(std::exception &cException)
{
    cerr << "Standard exception: " << cException.what() << endl;
}

In this example, exceptions of type std::bad_alloc will be caught by the first handler and handled there. Exceptions of type std::exception and all of the other derived classes will be caught by the second handler.

Such inheritance hierarchies allow us to use specific handlers to target specific derived exception classes, or to use base class handlers to catch the whole hierarchy of exceptions. This allows us a fine degree of control over what kind of exceptions we want to handle while ensuring we don’t have to do too much work to catch “everything else” in a hierarchy.

0 0
原创粉丝点击