为什么在 C++ 中捕获时派生异常类型会丢失?

Why is the derived exception type lost when catching in C++?

我目前正在研究异常类型,在尝试重新抛出捕获的异常时我注意到了一些奇怪的事情。 从 C++ 规范中,我知道 throw 实际上会生成您试图抛出的对象的副本,因此您最终将对捕获的任何剩余派生类型信息进行切片。 为避免这种情况,我看到了重新 throw 指向原始异常的指针的建议,因为实际的原始对象将不会删除其派生部分。 但是,我在下面编写的简单示例程序似乎不能那样工作:

#include <exception>
#include <iostream>
#include <typeinfo>

class derived_exception : public std::exception { };

void rethrowException(bool anonymise) {
  try {
    throw derived_exception();
  } catch(const std::exception& e) {
    std::cout << "Caught: " << typeid(e).name() << " (std::exception)" << std::endl;
    if(anonymise) {
      throw;
    } else {
      throw &e;
    }
  }
}

int main() {
  std::cout << "Re-throwing caught exception..." << std::endl;
  try {
    rethrowException(false);
  } catch(const derived_exception* e) {
    std::cout << "Re-caught: " << typeid(e).name() << " (derived_exception)" << std::endl;
  } catch(const std::exception* e) {
    std::cout << "Re-caught: " << typeid(e).name() << " (std::exception)" << std::endl;
  }
    std::cout << std::endl << "Re-throwing anonymous exception..." << std::endl;
  try {
    rethrowException(true);
  } catch(const derived_exception& e) {
    std::cout << "Re-caught: " << typeid(e).name() << " (derived_exception)" << std::endl;
  } catch(const std::exception& e) {
    std::cout << "Re-caught: " << typeid(e).name() << " (std::exception)" << std::endl;
  }
}

./example的输出:

Re-throwing caught exception...
Caught: 17derived_exception (std::exception)
Re-caught: PKSt9exception (std::exception)

Re-throwing anonymous exception...
Caught: 17derived_exception (std::exception)
Re-caught: 17derived_exception (derived_exception)

您可以成功地重新转换指针并检索派生类型信息,但指针类型仍然是初始切片。 有没有办法在不抓住基地并尝试 dynamic_cast 返回的情况下解决这个问题?

谢谢

1。 throw 抛出什么?

C++14 Standard

5.17 Throwing an exception

  1. A throw-expression with no operand rethrows the currently handled exception.

15.1 Throwing an exception

  1. Throwing an exception copy-initializes a temporary object, called the exception object. The temporary is an lvalue and is used to initialize the variable declared in the matching handler.

对于语句throw;,会重新抛出当前异常对象derived_exception()

对于语句 throw &e;,它会创建一个类型为 std::exception * 的临时对象,实际上等同于 throw (std::exception *except_obj = &e);.

2。哪个 catch 抓住了?

C++14 Standard

15.1 Throwing an exception

  1. Throwing an exception copy-initializes a temporary object, called the exception object. The temporary is an lvalue and is used to initialize the variable declared in the matching handler.

15.3 Handling an exception

  1. A handler is a match for an exception object of type E if

    • [3.1] The handler is of type cv T or cv T& and E and T are the same type (ignoring the top-level cv-qualifiers), or
    • [3.2] the handler is of type cv T or cv T& and T is an unambiguous public base class of E, or
    • [3.3] the handler is of type cv T or const T& where T is a pointer type and E is a pointer type that can be converted to T by either or both of
      • a standard pointer conversion (4.10) not involving conversions to pointers to private or protected or ambiguous classes
      • a qualification conversion, or
    • [3.4] the handler is of type cv T or const T& where T is a pointer or pointer to member type and E is std::nullptr_t.
  2. The handlers for a try block are tried in order of appearance.

anonymise == true:

throw;被执行,因此重新抛出异常对象derived_exception(),然后在:

中选择一个入口点
  • catch(const derived_exception& e),
  • catch(const std::exception& e).

根据标准 15.3-3-3.2,我们知道 derived_exception() 匹配 catch(const derived_exception& e)。因此输出是:

Re-caught: 17derived_exception (std::exception)

anonymise == false:

throw &e;被执行,从而创建一个类型为std::exception *的临时异常对象,然后在

中选择一个入口点
  • catch(const derived_exception* e),
  • catch(const std::exception* e).

我们不能选择前者。因为标准 15.3-3 中的四个规则中的 none 说 std::exception * 被认为是 const derived_exception *.

的匹配项

所以,后面的catch被选中了。我们看到输出:

Re-caught: PKSt9exception (std::exception)

(你可能想争论第三条规则[3.3],但是标准指针转换限定转换都不支持从指向子类指针的基类指针,必须显式向下转换,如使用dynamic_cast<T>()。)

3。在你的问题中

From the C++ specification, I know that throw actually produces a copy of the object you're attempting to throw,

没错。

so you will end up slicing any residual derived-type information that you caught.

如果您在 catch 中使用值类型而不是引用或指针,那就对了。一个例子是

try {
    throw derived_exception();
} catch (const std::exception e) {
    ...
}

从标准 15.1-3 中,我们知道此处 e 将使用 derived_exception() 进行初始化。实际上,这就像执行 e = derived_exception();。不过,我找不到使用此表格的任何理由。

I have seen suggestions to re-throw a pointer to the original exception as the actual original object will then not have its derived portion removed.

typeid(e).name()替换为typeid(*e).name(),可以看到原来的对象没有被切片:

catch (const std::exception *e)
{
    std::cout << "Re-caught: " << typeid(*e).name() << " (std::exception)"
              << std::endl;
}
// Re-caught: 17derived_exception (std::exception)