Exceptions and member functions
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:
到目前为止,在本教程中,你仅看到了非成员函数中使用的异常。 但是异常在重载运算符中也是如此。 将以下重载[]运算符视为简单整数数组类的一部分:
int& IntArray::operator[](const int index)
{
return m_data[index];
}
Although this function will work great as long as index 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:
尽管只要下标是有效的数组下标,函数就可以很好地工作。但这个函数仍然缺少一些错误检查功能。 我们可以添加一个assert语句来确保下标是有效的:
int& IntArray::operator[](const int index)
{
assert (index >= 0 && index < getLength());
return m_data[index];
}
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.
如果user传递了无效的下标,程序将导致一个assertion错误。 尽管这有助于向用户指示出问题,但更好的做法是静静的失败,让呼叫者知道出了问题,以便他们可以适当地进行处理。
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:
不幸的是,由于重载运算符对接收和返回的参数的数量和类型有特定的要求,因此没有办法将错误代码或布尔值传递回调用者。 但是,异常也不会更改函数的签名,所以在这里可以传回一个int类型的异常:
int& IntArray::operator[](const int index)
{
if (index < 0 || index >= getLength())
throw index;
return m_data[index];
}
Now, if the user passes in an invalid index, operator[] will throw an int exception.
这样的话,如果用户传入了不合适的下标,重载函数就会抛出一个int异常
Constructors are another area of classes in which exceptions can be very useful. If a constructor must fail for some reason (e.g. the user passed in invalid input), simply throw an exception to indicate the object failed to create. In such a case, the object’s construction is aborted, and all class members (which have already been created and initialized prior to the body of the constructor executing) are destructed as per usual. However, the class’s destructor is never called (because the object never finished construction).
异常在构造函数里面也很有用。如果一个构造函数因为某个原因必须失败,只需要抛出一个异常来表示对象创建失败,在这样的例子中,对象的创建被放弃掉了。所有的已经创建的成员(先于构造函数创建和初始化的成员变量)也会正常被析构。然而,这个类的析构函数不会被调用(因为这个对象就没有被创建完成)。
Because the destructor never executes, you can not rely on said destructor to clean up any resources that have already been allocated. Any such cleanup can happen in the constructor prior to throwing the exception in the first place. However, even better, because the members of the class are destructed as per usual, if you do the resource allocations in the members themselves, then those members can clean up after themselves when they are destructed.
因为析构函数不会执行,所以你不能依赖析构函数做一些清理工作。而在异常抛出之前,这样的清理工作会在constructor中进行。更好的是,因为类的成员会像往常一样被析构,如果你在成员自己身上做资源分配,这些成员可以在遭到破坏后可以自行清理。
Here’s an example:
#include <iostream>
class Member
{
public:
Member()
{
std::cerr << "Member allocated some resources\n";
}
~Member()
{
std::cerr << "Member cleaned up\n";
}
};
class A
{
private:
int m_x;
Member m_member;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1;
}
~A()
{
std::cerr << "~A\n"; // should not be called
}
};
int main()
{
try
{
A a{0};
}
catch (int)
{
std::cerr << "Oops\n";
}
return 0;
}
This prints:
Member allocated some resources
Member cleaned up
Oops
In the above program, when class A throws an exception, all of the members of A are destructed. This gives m_member an opportunity to clean up any resources that were allocated.
上面的例子中,如果A抛出一个异常,所有的A的成员都会被析构。
This is part of the reason that RAII (reference: 11.9 -- Destructors) is advocated so highly -- even in abnormal circumstances, classes that implement RAII properly should be able to clean up after themselves.
这是RAII被提倡的原因。在异常情形中,实现了RAII的类应该能够自己清理
One of the major problems 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.
基础类型作为异常类型的缺点在于它的表示很模糊。更坏的情况是如果try块里面有多个语句抛出了相同的异常,你能知道是哪个出的问题吗?
// Using the IntArray overloaded operator[] above
try
{
int *value{ new int{ array[index1] + array[index2]} };
}
catch (int value)
{
// 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 const 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.
在这个例子中,如果我们捕捉了int异常,他能告诉我们什么?是数组下标越界了吗?是+导致了溢出吗?是new失败了吗?不幸的是,在这个例子中,没有容易的办法来辨别。尽管我们抛出字符串异常来解决发生了什么的问题,但这仍然没有给我们提供解决不同异常源导致异常问题的能力。
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:
解决这个问题的办法是使用异常类,一个异常类就是一个普通的类。让我们设计一个简单的异常类:
#include <string>
class ArrayException
{
private:
std::string m_error;
public:
ArrayException(std::string error)
: m_error{error}
{
}
const char* getError() const { return m_error.c_str(); }
};
Here’s a full program using this class:
#include <iostream>
#include <string>
class ArrayException
{
private:
std::string m_error;
public:
ArrayException(std::string error)
: m_error(error)
{
}
const char* getError() const { return m_error.c_str(); }
};
class IntArray
{
private:
int m_data[3]; // assume array is length 3 for simplicity
public:
IntArray() {}
int getLength() const { return 3; }
int& operator[](const int index)
{
if (index < 0 || index >= getLength())
throw ArrayException("Invalid index");
return m_data[index];
}
};
int main()
{
IntArray array;
try
{
int value{ array[5] };
}
catch (const ArrayException &exception)
{
std::cerr << "An array exception occurred (" << exception.getError() << ")\n";
}
}
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 its own unique type, we can specifically catch exceptions thrown by the array class and treat them differently from other exceptions if we wish.
通过使用这样的类,我们可以让异常返回一个问题的描述。同时也因为ArrayException是一个独一无二的类,我们可以指定catch捕捉的异常类型,把这种类型的异常和别的异常分开来对待。
Note that exception handlers should catch class exception objects by reference instead of by value. This prevents the compiler from making a copy of the exception, which can be expensive when the exception is a class object, and prevents object slicing when dealing with derived exception classes (which we’ll talk about in a moment). Catching exceptions by pointer should generally be avoided unless you have a specific reason to do so.
注意异常处理器应该用引用而不要用值来捕获类对象的异常。这样的话编译器就不用做拷贝,拷贝的代价在类复杂的时候可以变的很大。也阻止了在处理派生类异常用基类捕获的时候导致的对象截断问题。用指针来捕获异常应该被避免,除非你有特别的原因。
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:
因为可以把类对象抛出,而类可以从别的类继承,我们需要思考一下如果我们用被继承的类作为异常类型。异常处理器不仅会匹配这个类,而且还能匹配这个类的派生类。
class Base
{
public:
Base() {}
};
class Derived: public Base
{
public:
Derived() {}
};
int main()
{
try
{
throw Derived();
}
catch (const Base &base)
{
std::cerr << "caught Base";
}
catch (const Derived &derived)
{
std::cerr << "caught Derived";
}
return 0;
}
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.
上面说过,派生类会被基类的catch处理器抓住。因为派生类是从基类派生出来的,派生类就是基类。因为C++试图找到一个处理器能处理被抛出的异常,它一个一个挨着找,所以C++做的第一件事就是看这个对基类的异常处理器能否匹配派生类异常。因为派生类是基类,所以就匹配上来。然后就开始执行异常处理。在上面的例子中,对派生类的异常处理器没被执行。
In order to make this example work as expected, we need to flip the order of the catch blocks:
为了让这个例子符合我们的期望,我们要把这个catch块的顺序颠倒一下。
class Base
{
public:
Base() {}
};
class Derived: public Base
{
public:
Derived() {}
};
int main()
{
try
{
throw Derived();
}
catch (const Derived &derived)
{
std::cerr << "caught Derived";
}
catch (const Base &base)
{
std::cerr << "caught Base";
}
return 0;
}
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.
使用对基类的异常处理器来捕捉基类和派生类的异常是非常有用的
Many of the classes and operators in the standard library throw exception classes on failure. For example, operator new can throw std::bad_alloc if it is unable to allocate enough memory. A failed dynamic_cast will throw std::bad_cast. And so on. As of C++17, there are 25 different exception classes that can be thrown, with more being added in each subsequent language standard.
标准库中的很多类和运算符都会在失败的时候抛出异常。举个例子,new在没有足够内存分配的时候会抛出 std::bad_alloc异常。失败的dynamic_cast 会抛出bad_cast异常,等等。到C++17,有25中异常类可以被抛出,在后续的语言标准中,也添加了很多的类。
The good news is that all of these exception classes are derived from a single class called std::exception. std::exception is a small interface class designed to serve as a base class to any exception thrown by the C++ standard library.
好消息是所有的这些异常类都是从std::exception中派生出来的。exception是一个接口类,也是设计来作为一个基类提供给所有的C++标准库中的异常类继承的。
Much of the time, when an exception is thrown by the standard library, we won’t care whether it’s a bad allocation, a bad cast, or something else. We just care that something catastrophic went wrong and now our program is exploding. Thanks to std::exception, we can set up an exception handler to catch exceptions of type std::exception, and we’ll end up catching std::exception and all (21+) of the derived exceptions together in one place. Easy!
大部分时候,由标准库抛出的异常,我们不需要关心它是bad_allocation,还是bad_cast还是别的什么东西。我们只关心灾难性错误。感谢 std::exception,我们可以设置一个异常处理器来处理所有的 std::exception派生类,在一个地方处理超过21个派生类的异常。
#include <iostream>
#include <exception> // for std::exception
#include <string> // for this example
int main()
{
try
{
// Your code using standard library goes here
// We'll trigger one of these exceptions intentionally for the sake of example
std::string s;
s.resize(-1); // will trigger a std::length_error
}
// This handler will catch std::exception and all the derived exceptions too
catch (const std::exception &exception)
{
std::cerr << "Standard exception: " << exception.what() << '\n';
}
return 0;
}
The above program prints:
Standard exception: string too long
The above example should be pretty straightforward. The one thing worth noting is that std::exception has a virtual member function named what() that returns a C-style string description of the exception. Most derived classes override the what() function to change the message. Note that this string is meant to be used for descriptive text only -- do not use it for comparisons, as it is not guaranteed to be the same across compilers.
上面的例子很简单,值得注意的是 std::exception有一个虚函数what(),返回一个C-style的字符串描述。大部分的派生类重载了what函数来改变一些错误消息,注意string是用来描述状况的,不要拿它来做比较。在不同的编译器上,这种字符串的描述不是固定的。
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:
有时候我们想单独处理某个特定的异常类型,在这样的情况下,我们可以添加一个handler给这个特定类型,然后让所有其他类型的异常“落到”基类异常处理器里面
try
{
// code using standard library goes here
}
// This handler will catch std::length_error (and any exceptions derived from it) here
catch (const std::length_error &exception)
{
std::cerr << "You ran out of memory!" << '\n';
}
// This handler will catch std::exception (and any exception derived from it) that fall
// through here
catch (const std::exception &exception)
{
std::cerr << "Standard exception: " << exception.what() << '\n';
}
In this example, exceptions of type std::length_error 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.
在本例中length_error会被第一个处理器处理,所有std::exception的派生类会被抓到第二个处理器中
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.
这样的继承结构允许我们使用特定的处理器来处理特定的派生类异常,或者使用基类异常处理器来捕获所有下面的异常类型。这使我们可以很好地控制我们要处理的异常类型,同时确保我们不必做太多工作就可以捕获继承层次结构中的“其他所有内容”。
Nothing throws a std::exception directly, and neither should you. However, you should feel free to throw the other standard exception classes in the standard library if they adequately represent your needs. You can find a list of all the standard exceptions on cppreference.
没有东西会直接抛出std :: exception,你也不会(他是个接口类不能实例化)。 但是,如果其他标准库中的异常类足以代表你的需求,你就可以抛出它。 你可以在[cppreference](http://en.cppreference.com/w/cpp/error/exception)上找到所有标准例外的列表。
std::runtime_error (included as part of the stdexcept header) is a popular choice, because it has a generic name, and its constructor takes a customizable message:
std :: runtime_error(包含在stdexcept头文件中)是一个很受欢迎的选择,因为它具有通用的名称,而且其构造函数接受可自定义的消息作为参数:
#include <iostream>
#include <stdexcept>
int main()
{
try
{
throw std::runtime_error("Bad things happened");
}
// This handler will catch std::exception and all the derived exceptions too
catch (const std::exception &exception)
{
std::cerr << "Standard exception: " << exception.what() << '\n';
}
return 0;
}
This prints:
Standard exception: Bad things happened
You can, of course, derive your own classes from std::exception, and override the virtual what() const member function. Here’s the same program as above, with ArrayException derived from std::exception:
当然,你可以从std :: exception派生自己的类,并覆盖虚的what()const成员函数。 这是与上面相同的程序,而其中ArrayException继承了std :: exception:
#include <exception> // for std::exception
#include <iostream>
#include <string>
#include <string_view>
class ArrayException : public std::exception
{
private:
std::string m_error{};
public:
ArrayException(std::string_view error)
: m_error{error}
{
}
// return the std::string as a const C-style string
const char* what() const noexcept override { return m_error.c_str(); }
};
class IntArray
{
private:
int m_data[3]; // assume array is length 3 for simplicity
public:
IntArray() {}
int getLength() const { return 3; }
int& operator[](const int index)
{
if (index < 0 || index >= getLength())
throw ArrayException("Invalid index");
return m_data[index];
}
};
int main()
{
IntArray array;
try
{
int value{ array[5] };
}
catch (const ArrayException &exception) // derived catch blocks go first
{
std::cerr << "An array exception occurred (" << exception.what() << ")\n";
}
catch (const std::exception &exception)
{
std::cerr << "Some other std::exception occurred (" << exception.what() << ")\n";
}
}
In C++11, virtual function what() was updated to have specifier noexcept (which means the function promises not to throw exceptions itself). Therefore, in C++11 and beyond, our override should also have specifier noexcept.
在C ++ 11中,虚函数what()已更新为具有指定符noexcept(该函数承诺不会自己抛出异常)。 因此,在C ++ 11及更高版本中,我们的重写函数也应该具有说明符noexcept。
It’s up to you whether you want create your own standalone exception classes, use the standard exception classes, or derive your own exception classes from std::exception. All are valid approaches depending on your aims.
是否要创建自己的异常类,使用标准库异常类还是从std :: exception派生自己的异常类,这些完全取决于你。 所有的这些都是有效的方法,具体取决于你的目标是什么。