【问题标题】:C++: Throwing a derived class by reference does not work when catching base classC++:捕获基类时,通过引用抛出派生类不起作用
【发布时间】:2012-04-07 11:35:30
【问题描述】:

我想用基类Exception 抛出我自己的异常。有一个虚拟方法print 将被子类覆盖。我只捕获Exception& 类型并使用print 来获取特定错误。问题是,一旦我抛出一个子类的引用,它就会被视为基类。

这是一个例子:

#include <iostream>
using namespace std;

class Exception
{
    public:
        virtual void print()
        {
            cout << "Exception" << endl;
        }
};

class IllegalArgumentException : public Exception
{
    public:
        virtual void print()
        {
            cout << "IllegalArgumentException" << endl;
        }
};

int main(int argc, char **argv)
{
    try
    {
        IllegalArgumentException i;
        Exception& ref = i;

        cout << "ref.print: ";
        ref.print();

        throw ref;
    }
    catch(Exception& e)
    {
        cout << "catched: ";
        e.print();
    }
}

这个例子的输出是:

ref.print: IllegalArgumentException
catched: Exception

使用引用应该会导致使用派生类的print 方法。在 try 块内,引用确实使用它。为什么被捕获的Exception&amp; 不像IllegalArgumentException 那样行事,我怎样才能得到这种行为?

下面的代码似乎做了它应该做的事情:

try
{
    IllegalArgumentException i;
    Exception* pointer = &i;

    throw pointer;
}
catch(Exception* e)
{
    cout << "pointer catched: ";
    e->print();
}

但是在 try 块的范围之外指针不会变得无效吗?这样做是有风险的,如果我在堆上分配内存来解决这个问题,我有责任在 catch 块内删除,这也不是很漂亮。那么你将如何解决这个问题呢?

【问题讨论】:

  • 你不能“通过引用抛出”。
  • @MooingDuck,感谢您提供的链接 - 我能够提供答案。不幸的是,答案在这个例子中不起作用。因此,我必须得出结论,这些问题并不相同。

标签: c++ try-catch throw


【解决方案1】:

throw 隐式复制,因此切片。引用 C++11,§15.1/3:

throw-expression 初始化一个临时对象,称为exception object,其类型由移除任何顶级cv-qualifiers 来自 throw 操作数的静态类型,并将类型从“T”或“函数返回T”调整为“指向T”或“指向函数返回T的指针” , 分别。临时值是一个左值,用于初始化匹配的 handler 中命名的变量。如果异常对象的类型是不完整类型或指向不完整类型的指针,而不是(可能是 cv 限定的)void,则程序格式错误。除了这些限制和 15.3 中提到的类型匹配限制外,throw 的操作数在调用中被完全视为函数参数或返回语句的操作数。

我已经看到一些代码库通过抛出指向异常而不是直接指向对象的指针来解决这个问题,但我个人只是重新考虑你的“需要”首先这样做。

【讨论】:

  • 确实,我不认为需要将 throw 表达式中使用的类型作为基类。抛出 i 而不是 ref 可以避免在复制期间切片,因为临时是正确的类型,然后当调用异常处理程序时,它会按预期打印“IllegalArgumentException”。
  • 这只是一个展示手头问题的例子。我想做的是让宏自动插入引发异常的文件和行,因此我定义了这个:#define THROW_EXCEPTION(e) throw e.addLine(__LINE__);。异常基类中的方法addLine 返回对自身的引用。现在这不适用于上述派生类。
  • @user1286875 : "异常基类中的方法addLine 返回对自身的引用。" 为什么? C++ 支持虚函数的协变返回类型,因此将其设为虚函数并在派生类中返回实际类型而不是基类型。
  • 您可以添加一个非成员模板:template&lt;typename ExceptionType&gt; ExceptionType&amp; addLine(ExceptionType&amp; e, int line) { e.addline(line); return e; },然后将您的宏定义为#define THROW_EXCEPTION(e) throw addLine(e, __LINE__)
  • @celtschk 就是这样!您的解决方案对我来说非常有效,非常感谢。我没有考虑过使用模板,但它是一个非常好的选择。
【解决方案2】:

你看到的叫做切片。

您显然习惯于多态性,您可以将指向子类的指针分配给指向基类的指针,并且保留(子)类型和所有数据。毕竟,它只是一个被复制的指针,而不是对象本身。然而,当直接在对象之间进行赋值时,就不会发生这样的事情。基类通常具有较短的实例,并且在基类类型的变量之后可能没有可用空间(在您的情况下,在堆栈上;在其他人的情况下,在堆上)。

因此,C++ 被定义为对对象执行切片。只复制基类部分,类型也“降级”为基类。

【讨论】:

    【解决方案3】:

    当您抛出一个对象时,会将该对象的副本复制到其他位置,以便堆栈展开可以继续,并且该副本可以传递给异常处理程序(通过引用,因此不再进行复制,或者通过值创建第二份)。

    如果您将异常设置为不可复制,则可以验证是否已制作副本。您将无法再抛出该类型的对象。

    当实现复制你抛出的对象时,它会查看表达式的静态类型,而不是动态类型。这意味着在您的代码中,它看到您正在抛出 Exception,因此生成的副本是 slicing 的示例(即,不是复制完整的对象,而是仅复制一个基类子类对象被复制)。

    如果您确保 throw 表达式的静态类型与完整对象的类型匹配,则可以避免切片,您可以通过简单地不将类型强制为 Exception 来做到这一点。

    这应该打印“caught: IllegalArgumentException”。

    try
    {
        IllegalArgumentException i;
    
        throw i;
    }
    catch(Exception& e)
    {
        cout << "caught: ";
        e.print();
    }
    

    【讨论】:

      【解决方案4】:

      您可以使用指针而不是引用来避免切片:

      int main(int argc, char **argv)
      {
          try
          {
              IllegalArgumentException* pIAE = new IllegalArgumentException();
              throw pIAE;
          }
          catch(IllegalArgumentException* i)
              {
                  i->print();
          }
      }
      

      捕获的指针指向同一个对象,因为复制构造函数没有被隐式调用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-05
        • 2021-09-25
        • 1970-01-01
        • 1970-01-01
        • 2018-01-23
        • 2012-01-07
        相关资源
        最近更新 更多