C++:抛出异常会调用复制构造函数?

Posted

技术标签:

【中文标题】C++:抛出异常会调用复制构造函数?【英文标题】:C++: Throwing an exception invokes the copy constructor? 【发布时间】:2012-06-01 18:25:52 【问题描述】:

我们有一个自定义错误类,每当我们抛出异常时都会使用它:

class AFX_CLASS_EXPORT CCLAError : public CObject

它定义了以下复制构造函数:

CCLAError(const CCLAError& src)  AssignCopy(&src);  // (AssignCopy is a custom function)

它最初是使用 MSVC6 (Visual Studio 2003) 编写和编译/链接的。我正在进行必要的更改以使其编译并链接到 MSVC8+ (VS 2008+)

当调用 msvc8 链接器时,我收到以下错误:

LNK2001: unresolved external symbol "private: __thiscall CObject::CObject(class CObject const &)" (??0CObject@@AAE@ABV0@@Z)

我明白错误告诉我什么:没有为 CObject 的某个子对象定义复制构造函数,因此它一直沿继承树向上直到遇到 CObject,因为没有定义复制构造函数。

我在编译定义并首先抛出 CCLAError 的库时第一次看到错误,这就是为什么我要继续进行,好像这就是原因。

我能够通过更改解决错误

throw CCLAError( ... )

throw new CCLAError( ... )

catch(CCLAError& e)

   throw e;

catch(CCLAError& e)

   throw;

但是,我不明白为什么重新抛出捕获的异常会调用复制构造函数。我错过了一些完全明显的东西吗?随后,为什么删除包含对捕获的异常的引用的变量会导致复制构造函数不被调用?

【问题讨论】:

您不需要也不应该在抛出原始异常时使用new。但是您肯定需要将 throw e 更改为 throw 以重新抛出现有异常而不创建它的新实例。 【参考方案1】:

抛出的对象的类型必须是可复制的,因为throw 表达式可能会复制其参数(副本可能会被省略,或者在 C++11 中可能会发生移动,但复制构造函数必须仍然可访问和可调用)。

使用throw; 重新抛出异常不会创建任何副本。使用throw e; 抛出捕获的异常对象将导致生成e 的副本。这与重新抛出异常不同。

您的“更新”代码未按预期工作。 catch (CCLAError&) 不会捕获CCLAError* 类型的异常,这是throw new CCLAError(...); 抛出的异常类型。你需要抓住CCLAError*。但是,不要这样做。按值抛出异常并按引用捕获。所有异常类型都应该是可复制的。

【讨论】:

【参考方案2】:

但是,我不明白为什么重新抛出捕获的异常会调用复制构造函数。

它没有,但重新抛出抛出的异常是用throw; 完成的。当您执行 throw e; 时,您请求抛出捕获的异常的副本。

【讨论】:

有道理。由于 throw e 在 MSVC6 中有效,而现在在 MSVC8 中无效(如 throw 有效所示),它们是否都达到了相同的结果?还是旧代码泄漏内存? @johnluetke: 每当你throw 一个指针时它就会泄露(试图删除 很麻烦)。如果您按值捕获,throw e 可能会导致切片,这就是throw; 存在的原因。【参考方案3】:

几点:

首先不要调用throw new foo() 使用throw foo

第二次写作时:

catch(foo const &e) 
   throw e;

您实际上创建了一个新异常,例如,如果异常 被抛出是foo 的子类他们我的电话throw e 你会失去这个 信息并实际使用复制构造函数从 e 生成 foo - 不管 是的。

现在当你打电话时

catch(foo const &e) 
   throw;

您不会创建新异常,而是传播相同的异常。

所以:从不使用throw e;向前传播异常,使用throw;

【讨论】:

根据我对 K-ballo 答案的评论,旧代码(上面写着 throw e)是否会泄漏?【参考方案4】:

当您抛出异常时,您要抛出的对象通常驻留在堆栈中。作为抛出过程的一部分,堆栈正在被清理,因此编译器必须制作一个可以在该点之后继续存在的副本。

当您使用new 抛出一个对象时,您并不是在抛出实际的对象,而是在抛出一个指向该对象的指针。这意味着您的 catch 块还必须捕获指针而不是引用。不要忘记delete指针,否则你会发生内存泄漏!

当 catch 块使用 throw; 而不是 throw e; 时,它可以重复使用之前制作的副本,而无需制作另一个副本。

【讨论】:

【参考方案5】:

您似乎误解了重投的含义。当你这样做 -

catch(CCLAError& e)

   throw e;

-- 你没有重新投掷。相反,正如您所观察到的,您确实在创建新异常的副本。相反(同样,正如您自己发现的那样),这在技术上才是正确的重新投掷方式:

catch(CCLAError& e)

   throw;

阅读 Stroustrup 的 TC++PL 中的第 14 章(14.3.1 处理重新抛出)。

另外,你不必这样做 -

throw new CCLAError( ... )

相反,做 -

throw CCLAError( ... )

-- 就像你以前做的那样,只能通过 CONST 引用接收(你不能持有对临时的引用)。

catch(const CCLAError &e)

【讨论】:

关于你的最后一句话:没有临时对象:CCLAError& e 将绑定到异常对象。无需复制。你觉得哪里有暂时的? throw CCLAError ( ... ) 导致 OP 中描述的 LNK2001 链接器错误。【参考方案6】:

语言规范允许编译器根据需要制作尽可能多的原始对象副本。份数没有限制。

这意味着,您的自定义异常类必须有一个可访问复制构造函数,可以是编译器生成的,也可以是用户定义的。

【讨论】:

以上是关于C++:抛出异常会调用复制构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

构造函数析构函数抛出异常的问题

JMockit 期望 API:如何在方法/构造函数调用时抛出异常

为啥在构造函数中抛出异常会导致空引用?

Exception

在 C++ 中抛出后会调用析构函数吗?

c++析构函数需要异常处理吗?如需要实现有何要求?